CI dry-run revealed an architectural ordering bug: db-init/004 and
db-init/005 ALTER TABLE the Directus-managed tables (organization_users,
events, etc.), but db-init runs BEFORE schema-apply creates those
tables. On a fresh CI Postgres this fails with "relation does not
exist." Local dev never tripped this because we'd created the tables
via MCP first.
Fix: introduce a post-schema migration phase. Two db-init runs in the
entrypoint, with schema-apply in between:
1. apply-db-init.sh db-init/ → positions hypertable + faulty
column (tables Directus does
NOT manage)
2. schema-apply.sh → creates Directus-managed tables
from snapshots/schema.yaml
3. apply-db-init.sh db-init-post/ → composite UNIQUE constraints on
the Directus-managed tables
4. directus bootstrap
5. directus start
Files moved:
db-init/004_junction_unique_constraints.sql →
db-init-post/001_junction_unique_constraints.sql
db-init/005_event_participation_unique_constraints.sql →
db-init-post/002_event_participation_unique_constraints.sql
Each ALTER TABLE in the post-schema migrations is now wrapped in a
pg_constraint existence guard for idempotency. This handles the dev DB
where the constraints already exist (from the original 004/005 runs +
the manual psql recovery during task 1.5's destructive-apply
incident). Old 004/005 rows in migrations_applied become orphans —
harmless.
Updates:
- Dockerfile: COPY db-init-post into the image
- entrypoint.sh: 4-step → 5-step flow with the post-schema run between
schema-apply and bootstrap
- .gitea/workflows/build.yml: dry-run chains all three pre-boot scripts
(pre-schema → schema-apply → post-schema); path filter includes
db-init-post/**
- Task specs 1.4 and 1.5 Done sections: updated to reference the new
db-init-post/ path (db-init/004 → db-init-post/001, etc.)
The reusable runner script (apply-db-init.sh) didn't need to change —
it already accepts DB_INIT_DIR and uses just the basename for the
guard-table key. The two phases share migrations_applied; filenames
don't collide because pre-schema and post-schema use distinct
descriptive names.
Phase 1 is still "done" — this is a Phase 1 architectural correction
exposed by the CI dry-run, not a new task.
Phase 1 — Slice 1 schema + deploy pipeline
Stand up a Directus 11 instance with the minimum schema needed to register entries and tie them to devices, plus the schema-as-code pipeline (snapshots + db-init) and Gitea Actions CI. This is what Rally Albania 2026 needs to run as a test event.
Outcome statement
When Phase 1 is done:
- Directus runs locally via
docker compose -f compose.dev.yaml up, against a Postgres 16 + TimescaleDB + PostGIS container. db-init/contains three migrations applied at boot: TimescaleDB extension,positionshypertable creation,faulty booleancolumn on positions. All idempotent, all guarded by amigrations_appliedtable.snapshots/schema.yamlcontains 12 collections:organizations,users,organization_users,vehicles,organization_vehicles,devices,organization_devices,events,classes,entries,entry_crew,entry_devices. Relations and required fields per directus-schema-draft (the org-level catalog and event-participation sections).- The image entrypoint runs db-init, then
directus schema apply --yes, thendirectus start. All three exit 0 against a fresh Postgres. - Gitea Actions builds the image on push to
main(whensnapshots/,db-init/,extensions/,Dockerfile, or workflow file changes), runs the apply pipeline against a throwaway Postgres in CI, and pushes the image togit.dev.microservices.al/trm/directus:mainonly if the dry-run passes. - "Motorsport Club Albania" exists as an organization, "Rally Albania 2026" exists as an event under it, and the Rally Albania class catalog is seeded (M-1..M-7, Q-1..Q-3, C-1/C-2/C-A/C-3, S-1/S-2/S-3 from
wiki/sources/rally-albania-regulations-2025.md§2.2–§2.5). At least one test entry registered with vehicle + crew + devices, used to dogfood the registration workflow.
Phase 1 deliberately stops short of:
- Course definition (stages, segments, geofences, SLZs) — Phase 2.
- Penalty system tables and timing tables — Phase 3.
- Permission policies — Phase 4 (collections are admin-only by default).
- Custom extension code — Phase 5.
Sequencing
1.1 Project scaffold
└─→ 1.2 db-init runner script
└─→ 1.3 Initial migrations
├─→ 1.4 Org-level catalog collections (admin UI work)
│ └─→ 1.5 Event-participation collections (admin UI work)
│ └─→ 1.6 Schema snapshot/apply tooling
│ └─→ 1.7 Image build & entrypoint
│ └─→ 1.8 Gitea CI dry-run
│ └─→ 1.9 Rally Albania 2026 seed
Tasks 1.1 → 1.3 are pure infrastructure and can land before any Directus admin UI work begins. Tasks 1.4 + 1.5 happen against a locally running Directus instance. Tasks 1.6 → 1.8 wire the artifacts together. Task 1.9 is dogfood verification.
Files modified
Phase 1 produces this layout in directus/:
directus/
├── .gitea/workflows/build.yml
├── snapshots/
│ └── schema.yaml # generated; edits via admin UI + pnpm run schema:snapshot
├── db-init/
│ ├── 001_extensions.sql # CREATE EXTENSION timescaledb (postgis added in Phase 2)
│ ├── 002_positions_hypertable.sql
│ └── 003_faulty_column.sql
├── extensions/ # empty — Phase 5 fills this
├── scripts/
│ ├── apply-db-init.sh # numeric-order, guard-table-protected runner
│ ├── schema-snapshot.sh # wraps `directus schema snapshot --yes`
│ └── schema-apply.sh # wraps `directus schema apply --yes`
├── entrypoint.sh # apply-db-init.sh && directus schema apply && directus start
├── Dockerfile # FROM directus/directus:11.x + bundled artifacts
├── compose.dev.yaml # local dev: directus + timescaledb container
├── package.json # only for the snapshot/apply npm scripts and tooling
├── pnpm-lock.yaml
├── .env.example
├── .dockerignore
├── .gitignore
└── README.md
Tech stack (decided)
- Directus 11.x (latest stable on the 11.x line at time of build). Pinned in
DockerfileFROMline. - Postgres 16 + TimescaleDB + PostGIS as the database (PostGIS extension added in Phase 2; Phase 1 only uses TimescaleDB).
- pnpm for any local dev scripts (snapshot wrappers, lint).
- bash (POSIX-compatible) for
apply-db-init.shandentrypoint.sh. No Node dependency at runtime — only Directus needs Node, and that's the upstream image's responsibility. - psql (from
postgresql-clientpackage) inside the image for db-init application. - Gitea Actions for CI, matching the
processorandtcp-ingestionworkflow shape.
If an implementer wants to deviate, they must update the relevant task file first.
Key design decisions inherited from processor
- Image is bundled, not assembled at runtime.
snapshots/,db-init/, andextensions/are baked into the image, not mounted as volumes. Reproducible across envs. - Slim Dockerfile. Multi-stage if extensions need a build step (Phase 5+); for Phase 1 a single stage is enough.
- CI workflow — single-job pattern matching
processor/.gitea/workflows/build.yml. Useservices:for the throwaway Postgres in the dry-run step. - No
.envin image. All env vars come from the deploy stack (Portainer / compose) at runtime.
Open questions blocking task-level detail
None. The schema draft pinned the org-level catalog and event-participation shape; Phase 1 implements exactly that subset.