Skip to content

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.Repouse Ecto.Repo, otp_app: :finnest_core, adapter: Ecto.Adapters.Postgres, configured via runtime.exs reading env vars (DATABASE_URL / FINNEST_DB_*)
  • Finnest.Core.Tenant — small GenServer-free module wrapping Process.get/put/delete with current_org_id/0, current_org_id!/0 (raises if unset), put_org_id/1, clear/0
  • Finnest.Core.CorrelationId — module with same pattern: current/0, current!/0, put/1, clear/0; UUID generation via UUID.uuid4/0
  • FinnestWeb.Plugs.CorrelationId — Plug that reads x-correlation-id header or generates a UUID; writes to Process dict + conn.assigns + response header
  • Finnest.Core.Supervisorrest_for_one supervisor starting: Repo → Telemetry → (placeholder for EventStore, FeatureFlagCache, AuditLogger from later stories) → PubSub
  • Finnest.Core.Telemetry — skeleton: :telemetry_poller + a child_spec/1 that emits VM + Phoenix metrics; full OpenTelemetry integration is F-020 scope
  • Local dev Postgres config for mix ecto.create + mix ecto.migrate to work out-of-the-box against the Docker dev Postgres (F-005)

Out of scope:

  • Full prepare_query tenant 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.RequestId in the endpoint pipeline so every subsequent log line carries it
  • logger_json (Elixir logger backend) with a metadata filter to include correlation_id, org_id, user_id on every log line — configure here (foundation for OP-01)
  • Keep the module API tight (Commandment #10 surface-area-is-cost) — current_org_id!/0 raising is the only way other modules should interact
  • Finnest.Repo.prepare_query is 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.Repo module exists; mix ecto.create and mix ecto.migrate work against local Postgres
  • Finnest.Core.Tenant.current_org_id!/0 raises RuntimeError with clear message "No tenant context set" when called without put_org_id/1 first
  • Finnest.Core.Tenant.put_org_id(uuid) |> then(fn _ -> current_org_id!() end) returns the put UUID
  • Finnest.Core.CorrelationId module 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 to conn.resp_headers and Process dict
  • Finnest.Core.Supervisor starts successfully on Application.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_one restarts the dependents)
  • Logger backend (logger_json) configured; running Logger.info("test") after put_org_id/put_correlation_id produces JSON log line containing both fields as top-level metadata
  • mix format --check-formatted, mix credo --strict, mix dialyzer all green for finnest_core

Testing Requirements

  • Unit tests for Finnest.Core.Tenant and Finnest.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