# Task 1.4 — Org-level catalog collections **Phase:** 1 — Slice 1 schema + deploy pipeline **Status:** ⬜ Not started **Depends on:** 1.3 (db-init applied so Directus can boot) **Wiki refs:** `docs/wiki/synthesis/directus-schema-draft.md` (Org-level catalog section), `docs/wiki/sources/rally-albania-regulations-2025.md` ## Goal Create the durable, org-level collections in the Directus admin UI: `organizations`, `users` (using Directus's built-in users with custom fields), `organization_users`, `vehicles`, `organization_vehicles`, `devices`, `organization_devices`. These are the resources that exist independently of any single event. This task happens against a locally running Directus instance (from `pnpm dev`). The output is a snapshot YAML that captures the collection definitions; that snapshot lands in git in task 1.6. ## Deliverables Create the following collections via the admin UI (Settings → Data Model). Field shapes per [[directus-schema-draft]]. Required-field columns marked `*`. ### `organizations` | Field | Type | Notes | |---|---|---| | `id` * | UUID | primary key, auto-generated | | `name` * | string | display name | | `slug` * | string | URL-friendly identifier, unique | | `created_at` | timestamp | Directus standard | | `updated_at` | timestamp | Directus standard | Singleton: false. Sort: `name asc`. ### `users` (extending Directus built-in `directus_users`) Use the built-in user collection. Add custom fields (Settings → Data Model → directus_users): | Field | Type | Notes | |---|---|---| | `phone` | string | optional | | `birth_date` | date | optional, used for age-derived class eligibility (M-5/M-6/M-7) | | `nationality` | string | ISO 3166-1 alpha-2 country code | Do NOT add an `organization_id` here — multi-tenancy goes through `organization_users`. ### `organization_users` (junction) | Field | Type | Notes | |---|---|---| | `id` * | UUID | | | `organization_id` * | M2O → organizations | | | `user_id` * | M2O → directus_users | | | `role` * | string (dropdown) | enum: `org-admin`, `race-director`, `marshal`, `timekeeper`, `participant`, `viewer` | | `joined_at` | timestamp | default `now()` | Unique constraint: `(organization_id, user_id)` — a user can only have one row per org. Multiple roles per user in same org → not yet (single role per tenant; revisit if needed). ### `vehicles` | Field | Type | Notes | |---|---|---| | `id` * | UUID | | | `make` * | string | "Toyota" | | `model` * | string | "Land Cruiser 70" | | `year` | integer | | | `engine_cc` | integer | engine displacement, used for class assignment | | `vin` | string | optional | | `plate_number` | string | optional | | `notes` | text | | No `owner_user_id` / `owner_team_id` — vehicles are org-scoped only, ownership is not modeled (per [[directus-schema-draft]] decision). ### `organization_vehicles` (junction) | Field | Type | Notes | |---|---|---| | `id` * | UUID | | | `organization_id` * | M2O → organizations | | | `vehicle_id` * | M2O → vehicles | | | `registered_at` | timestamp | default `now()` | Unique constraint: `(organization_id, vehicle_id)`. ### `devices` | Field | Type | Notes | |---|---|---| | `id` * | UUID | | | `imei` * | string | unique, the canonical device identifier | | `model` * | string | "FMB920", "FMB003", etc. — drives IO mapping in [[processor]] | | `serial_number` | string | optional | | `notes` | text | | `imei` UNIQUE — same IMEI can't be registered twice anywhere in the system. ### `organization_devices` (junction) | Field | Type | Notes | |---|---|---| | `id` * | UUID | | | `organization_id` * | M2O → organizations | | | `device_id` * | M2O → devices | | | `registered_at` | timestamp | default `now()` | Unique constraint: `(organization_id, device_id)`. ## Specification - **Use UUIDs for all primary keys** (Directus offers UUID v4 generation natively). Avoids leaking row counts and simplifies cross-env data sync. - **All M2O relations have `ON DELETE` set to `RESTRICT`** by default — accidentally deleting an org or vehicle should require the operator to clean up dependents first. Override per-relation only with explicit reason. - **No permission policies** — Phase 4 territory. Set every collection to "All Access" → none (admin only) for now. - **No interface customization beyond defaults** — the SPA isn't using these collections directly yet, and admin UI usability for operators happens after Phase 4 (when policies define what they see). - **Do not commit `.env` or any secrets.** This task only modifies Directus schema, which is captured in the snapshot. ## Acceptance criteria - [ ] All seven collections exist in the admin UI with the fields listed above. - [ ] Required fields are flagged required. - [ ] All unique constraints are enforced (test by trying to create a duplicate row — should error). - [ ] M2O relations are visible and clickable in the admin UI's relational fields. - [ ] No permission policies attached (admin-only). - [ ] Manually create one organization, one user, one organization_user row → the relationships work end-to-end. - [ ] `pnpm run schema:snapshot` produces a `snapshots/schema.yaml` with all seven collections present (verified by grep). - [ ] Booting a brand-new Directus instance (fresh DB, fresh containers) and running `directus schema apply --yes snapshots/schema.yaml` recreates the seven collections identically. ## Risks / open questions - **`directus_users` field additions** — Directus does allow adding fields to its built-in user collection, but the snapshot/apply behavior for those additions has historically been finicky across versions. Verify on the pinned Directus version that custom user fields round-trip cleanly via `schema snapshot` + `schema apply`. If they don't, fall back to a separate `user_profiles` collection M2O'd to `directus_users`. - **Slug uniqueness on `organizations`** — Directus enforces this at the field level. Confirm it generates a unique-index DDL in the snapshot. ## Done (Fill in commit SHA + one-line note when this lands.)