STORY-F-003: finnest_core foundations (Repo, correlation ID, tenant primitives, supervisor)¶
Epic: Foundation Priority: Must Have Story Points: 3 Status: Not Started Assigned To: Unassigned Created: 2026-04-17 Sprint: 1
User Story¶
As the Lead Developer,
I want finnest_core carrying the foundational primitives every other app depends on — Ecto Repo, correlation-ID plug, tenant-context helper, and a supervisor wired to start them in the right order,
so that subsequent apps have a stable base to build on and architectural invariants (correlation IDs on every log, tenant scoping everywhere) are wired from the first line of business code.
Description¶
Background¶
finnest_core is the only app every other app depends on. Its job is to hold primitives — not business logic. This story establishes: the primary Ecto Repo pointing at Postgres 17, correlation ID generation and propagation through the Process dictionary, tenant-context helpers (read/put/raise), and a supervision tree that starts them in order per the architecture doc's rest_for_one + ordering guidance.
This does not include: the full Finnest.Repo.prepare_query tenant-scoping hook (STORY-F-008), auth schemas (STORY-F-006), event store (STORY-F-016). Those land in later stories, but the primitives they depend on come from here.
Scope¶
In scope:
Finnest.Repo—use Ecto.Repo, otp_app: :finnest_core, adapter: Ecto.Adapters.Postgres, configured viaruntime.exsreading env vars (DATABASE_URL / FINNEST_DB_*)Finnest.Core.Tenant— small GenServer-free module wrappingProcess.get/put/deletewithcurrent_org_id/0,current_org_id!/0(raises if unset),put_org_id/1,clear/0Finnest.Core.CorrelationId— module with same pattern:current/0,current!/0,put/1,clear/0; UUID generation viaUUID.uuid4/0FinnestWeb.Plugs.CorrelationId— Plug that readsx-correlation-idheader or generates a UUID; writes to Process dict +conn.assigns+ response headerFinnest.Core.Supervisor—rest_for_onesupervisor starting: Repo → Telemetry → (placeholder for EventStore, FeatureFlagCache, AuditLogger from later stories) → PubSubFinnest.Core.Telemetry— skeleton::telemetry_poller+ achild_spec/1that emits VM + Phoenix metrics; full OpenTelemetry integration is F-020 scope- Local dev Postgres config for
mix ecto.create+mix ecto.migrateto work out-of-the-box against the Docker dev Postgres (F-005)
Out of scope:
- Full
prepare_querytenant scoping hook (F-008) - Event store schema + trigger (F-016)
- Auth (F-006), users, organisations (F-007)
- OpenTelemetry exporters, Sentry (F-020)
Technical Notes¶
- Tenant primitives use process dictionary — not ETS or GenServer — because tenant context is per-request and inherently process-scoped. Tests can
Finnest.Core.Tenant.put_org_id(org.id)to set context. - Correlation ID plug should be the first plug after
Plug.RequestIdin the endpoint pipeline so every subsequent log line carries it logger_json(Elixir logger backend) with a metadata filter to includecorrelation_id,org_id,user_idon every log line — configure here (foundation for OP-01)- Keep the module API tight (Commandment #10 surface-area-is-cost) —
current_org_id!/0raising is the only way other modules should interact Finnest.Repo.prepare_queryis referenced but only stubbed in this story (returns query unchanged) — F-008 lands the full implementation. This keeps things working while F-007 writes the first real schemas.
Dependencies¶
- Blocked by: STORY-F-001 (umbrella), STORY-F-002 (boundary)
Acceptance Criteria¶
-
Finnest.Repomodule exists;mix ecto.createandmix ecto.migratework against local Postgres -
Finnest.Core.Tenant.current_org_id!/0raisesRuntimeErrorwith clear message "No tenant context set" when called withoutput_org_id/1first -
Finnest.Core.Tenant.put_org_id(uuid) |> then(fn _ -> current_org_id!() end)returns the put UUID -
Finnest.Core.CorrelationIdmodule matches Tenant's shape (current/current!/put/clear) -
FinnestWeb.Plugs.CorrelationId— when no inbound header, generates UUID v4; when inbound header present, propagates it; writes toconn.resp_headersand Process dict -
Finnest.Core.Supervisorstarts successfully onApplication.ensure_all_started(:finnest_core); shutdown is clean - Child start order verified: Repo before PubSub (if a child depends on an earlier one failing,
rest_for_onerestarts the dependents) - Logger backend (
logger_json) configured; runningLogger.info("test")afterput_org_id/put_correlation_idproduces JSON log line containing both fields as top-level metadata -
mix format --check-formatted,mix credo --strict,mix dialyzerall green forfinnest_core
Testing Requirements¶
- Unit tests for
Finnest.Core.TenantandFinnest.Core.CorrelationId: put/get/clear/raise behaviours - Plug test for
FinnestWeb.Plugs.CorrelationId: inbound header, absent header, response header set - Supervisor start test:
Application.ensure_all_started(:finnest_core)returns{:ok, _} - No architecture tests yet (those land in F-007/F-008)
References¶
../architecture/architecture.md§System Components, §Architectural Invariants (#11 correlation IDs)../architecture/data.md§Tenant enforcement../10-GUARDRAILS.mdOP-01, SE-29, DA-11- Commandment #24 (log by ID, never PII), #29 (correlation ID traceability)