Plan: Split atlas-frontend — rename existing to contributor app, scaffold fresh customer app
IMPLEMENTATION RULES: Before implementing this plan, read and follow:
- WORKFLOW.md - The implementation process
- PLANS.md - Plan structure and best practices
Status: Completed (2026-04-30)
Shipped on Atlas main as PR #33, squash-merged at commit 2266f21. All six phases complete; all acceptance criteria satisfied.
Outcome: the repo now has two top-level Next.js apps:
atlas-contributor-frontend/— today's originalatlas-frontend/renamed wholesale. Direct Postgres againstmarts.*, dev/staging only, port 4000. Used by ingest authors and dbt model writers to verify ingestion + dbt output. Homepage rewritten to clearly identify its contributor audience and surface 8 diagnostic tools as visible card-style affordances.atlas-frontend/— fresh scaffold consumingapi-atlas.helpers.novia typed fetch, port 3001, deploys toatlas.helpers.no. Forkable as a reference implementation for external developers (zero DB drivers, no monorepo cross-imports, self-containedpackage.json+ README). Three views perapi_v1.*endpoint, all introspection-driven (no hardcoded endpoint names anywhere):/datacatalog,/data/[endpoint]table viewer with pagination + sort + search,/data/[endpoint]/specper-endpoint OpenAPI spec slice.
Phase 5 deliverables shipped beyond the original PLAN scope:
- Generic table viewer at
/data/[endpoint]with shadcn-style Table primitives (no Radix dep) - URL-driven pagination, click-to-cycle column sort, search via PostgREST
or= ilikeacross string columns - Per-endpoint spec viewer at
/data/[endpoint]/spec
website/docs/developers/ stub created per Phase 6.6; full content tracked by INVESTIGATE-developer-docs-surface.md (still in backlog/, the natural follow-on PLAN-006).
Investigation: INVESTIGATE-frontend-data-access-architecture.md — settled all architectural commitments (URL anchors, naming, monorepo, no shared code, customer-app forkability). Now also in completed/.
Last Updated: 2026-04-30 — moved active/ → completed/
Prerequisites:
- PostgREST live with
api_v1.*wrapper layer (PLAN-004 + UIS PLAN-002, verified 2026-04-30 — seetalk2.md, Messages 1–4). api-atlas.localhostreachable from host machine.
Blocks:
- The actual
atlas.helpers.noproduction deploy depends on Phase 6 of this PLAN finishing and INVESTIGATE-deployment-pipeline.md shipping a deployment story.
Phase 1: Rename atlas-frontend/ → atlas-contributor-frontend/
Whole-folder git move. Today's content unchanged; just lives under a new name that matches its actual job.
Tasks
- 1.1
git mv atlas-frontend atlas-contributor-frontendfrom a feature branch. - 1.2 Update inbound references across the repo:
website/docs/contributors/setup.md— currently mentionsatlas-frontend/for the optional frontend setup; update path + frame as the contributor diagnostic app.website/docs/contributors/index.md— same.website/docs/contributors/data-journey.md— Stage 8 (Next.js queries).CLAUDE.mdat repo root — section "Atlas-frontend" or "Key Folders".atlas-data/CONTRIBUTING.mdif it references the path.- Any other markdown that has
atlas-frontend/as a literal path. - Run
grep -rn "atlas-frontend" website/ docs/ atlas-data/ CLAUDE.mdand fix every hit.
- 1.3 Verify
cd atlas-contributor-frontend && npm install && npm run devstill works against direct Postgres. Spot-check/coverage-gap/barnefattigdomand/admin/supply/redcross-branchesrender rows. - 1.4 Add a one-paragraph banner at the top of
atlas-contributor-frontend/README.mdthat says "this is the contributor app — direct DB access for ingestion verification — not for prod deploy. Customer-facing app is the newatlas-frontend/."
Validation
grep -rn "atlas-frontend" website/ docs/ atlas-data/ CLAUDE.md # should match the new customer app references only after Phase 2; for now expect zero hits
cd atlas-contributor-frontend && npm install && npm run dev
# In another terminal:
curl -fsS http://localhost:3000/coverage-gap/barnefattigdom > /dev/null && echo ok
Done when
- Branch contains the rename + all inbound references updated.
npm run devin the renamed folder works as before.- No literal references to
atlas-frontend/remain pointing at the old (now nonexistent) path.
Phase 2: Scaffold fresh atlas-frontend/ as a PostgREST consumer
Fresh Next.js app at the repo root. No carryover from atlas-contributor-frontend/. Independence from day one.
Tasks
- 2.1
npx create-next-app@latest atlas-frontend --typescript --app --no-src-dir(or--src-dirto match Atlas conventions — match whatatlas-contributor-frontend/uses for symmetry, even though they don't share code). - 2.2 Configure environment:
.env.exampleat root:NEXT_PUBLIC_API_URL=http://api-atlas.localhost.- Do not include
DATABASE_URLor any DB role. - Do not install
postgres.jsor any DB driver.
- 2.3 Port assignment: change
npm run devto use port 3001 ("dev": "next dev -p 3001"inpackage.json). Avoids collision withatlas-contributor-frontend/'s default 3000. Document in the README. - 2.4 Write the customer app's
README.mdto market the folder as a forkable reference:- "What this is": Atlas's customer-facing Next.js consuming
api-atlas.helpers.no. - "How to run locally":
cp .env.example .env.local && npm install && npm run dev. - "How to fork": clone or copy the folder, set
NEXT_PUBLIC_API_URLto your own PostgREST endpoint. - "What you can build": explain that
api-atlas.helpers.nois the same API external developers use; this folder is the canonical example.
- "What this is": Atlas's customer-facing Next.js consuming
- 2.5 Forkability discipline: enforce no upward imports.
- Add a top-of-file comment in any shared lib explaining the forkability constraint.
- Optional ESLint rule:
no-restricted-importsblocking../atlas-data,../website,../atlas-contributor-frontend.
Validation
cd atlas-frontend && npm install && npm run dev
# Visit http://localhost:3001 — Next.js default page renders.
# Verify package.json contains zero DB-driver dependencies:
jq '.dependencies | keys[]' atlas-frontend/package.json | grep -iE 'postgres|prisma|drizzle|pg' # should print nothing
# Verify .env.example does not contain DATABASE_URL:
grep -i database_url atlas-frontend/.env.example # should print nothing
Done when
- Fresh
atlas-frontend/with default Next.js scaffold renders atlocalhost:3001. - Zero DB driver dependencies in
package.json. .env.exampleonly referencesNEXT_PUBLIC_API_URL.- README markets the app as forkable.
Phase 3: Build src/lib/api.ts — typed fetch helpers against api_v1.*
Types are generated from PostgREST's OpenAPI spec via openapi-typescript. Single source of truth: the live spec at GET /. Hand-typed types are zero — every column type, format, and JSDoc description flows from schema.yml → dbt-osmosis → COMMENT ON COLUMN → PostgREST spec → api-types.ts.
Tasks
- 3.1 Add dev dependency:
npm install -D openapi-typescript. - 3.2 Generate types:
npx openapi-typescript http://api-atlas.localhost/ -o src/lib/api-types.ts. - 3.3 Add an npm script:
"api:types": "openapi-typescript http://api-atlas.localhost/ -o src/lib/api-types.ts". - 3.4 Write
src/lib/api.ts— a thinfetchwrapper that:- Reads
process.env.NEXT_PUBLIC_API_URL. - Returns typed responses using
api-types.ts. - Surfaces clear errors on 4xx/5xx (use Next.js error boundary patterns).
- Provides a helper to fetch row counts via
Prefer: count=exactheaders (used by the catalog page in Phase 4). - Has no SQL. Has no
postgres.jsimport. Imports only fromnode_modulesand./api-types.
- Reads
- 3.5 Document in the README: "after schema changes on the API side, refresh types with
npm run api:types."
Validation
cd atlas-frontend
npm run api:types # regenerates types successfully
cat src/lib/api-types.ts | head # contains paths and components
# Smoke test from a temp page or a node script:
node -e "fetch('http://api-atlas.localhost/indicator_summary?limit=1').then(r=>r.json()).then(console.log)"
Done when
src/lib/api.ts+src/lib/api-types.tsexist.npm run api:typesregenerates the types file from a live PostgREST.- Hand-typed types are zero (everything sourced from the OpenAPI generator).
Phase 4: First customer-facing route — /data catalog page
Build a self-describing data catalog at /data driven entirely by PostgREST introspection: GET / for the endpoint list + column types + descriptions, Prefer: count=exact for row counts, and api_v1.indicator_summary for the per-source indicator breakdown. Zero new mart views; zero atlas-data work in this phase. The catalog is the customer app's value-prop demonstration: "this app dogfoods the API by being the catalog of what's in the API."
Tasks
- 4.1 Build
app/data/page.tsx:- Fetch the OpenAPI spec at
process.env.NEXT_PUBLIC_API_URL(root). Extractpaths(the endpoint list) anddefinitions(column types + descriptions). - For each path (excluding
/), fetch its row count vialib/api.ts'sPrefer: count=exacthelper. - For
/indicator_summary, fetch the rows and group bysource_idto render the per-source indicator catalogue (sources, contents codes per source, latest year). - Render as a clean, public-facing landing — table or card grid. Each cataloged endpoint links to a detail page (built piecemeal across Phase 5).
- Loading + error states. Server component (Next.js fetches at request time; no client-side state needed).
- Fetch the OpenAPI spec at
- 4.2 Add a homepage (
app/page.tsx) that introduces Atlas and links to/data. One-liner now; can grow with marketing later. - 4.3 Verify the catalog reflects every
api_v1.*endpoint without hardcoding any of them — adding a new mart view + regeneratingapi_v1should make it appear in the catalog automatically on next page load.
Validation
curl -fsS http://localhost:3001/data > /dev/null && echo ok # catalog renders
curl -s http://localhost:3001/data | grep -i "indicator_summary" # spec-derived endpoint name appears
curl -s http://localhost:3001/data | grep -i "kommune_local_chapters" # all 9 endpoints appear
grep -rn "DATABASE_URL\|postgres(" atlas-frontend/ # should print nothing
grep -rn "indicator_summary\|coverage_gap" atlas-frontend/app/data/page.tsx # endpoints are NOT hardcoded — should print 0 hits
Done when
/datalists everyapi_v1.*endpoint with row count and column-level descriptions sourced from the live OpenAPI spec.- The page is fully introspection-driven — no hardcoded endpoint names, no manual table of contents.
atlas-frontend/has zeroDATABASE_URLreference and zeropostgres.jsusage.- The PLAN-004 validation gates still pass (no
api_v1.*changes in this phase).
Phase 5: Scale out — detail pages linked from the catalog
Open-ended phase. Each cataloged endpoint can grow a detail page; new mart views in atlas-data/ unlock new endpoints. Pace driven by what customer-facing pages we actually want to ship, not by mirroring the contributor app one-for-one.
Two natural early scale-outs (illustrative; pick when ready):
/coverage-gap/barnefattigdom— pure detail page, zero atlas-data work.api_v1.coverage_gap_barnefattigdomis already in the surface. Renders the kommune-level child-poverty cut as a sortable table or bar chart. Demonstrates the "fetch-detail-from-existing-mart" pattern./kommuner/[kommune_nr]— exercises the new-mart-view workflow end-to-end:- Add
atlas-data/dbt/models/marts/api/mart_kommune_overview.sql(kommune metadata: kommune_nr, name, fylke_name, population, etc.). - Add
schema.ymldescription for every column (PLAN-004's runtime description gate enforces this). - Run
dbt run --select mart_kommune_overview+./regenerate-api-v1.sh+./apply-api-v1.sh. - Verify
curl http://api-atlas.localhost/kommune_overview?kommune_nr=eq.0301returns rows. npm run api:typesregenerates the customer app's types — the new endpoint appears automatically.- Implement
app/kommuner/[kommune_nr]/page.tsx. The catalog at/datapicks up the new endpoint on next page load with no code change.
- Add
Generic per-route flow
- 5.x.1 Confirm the route's data needs map to an existing
api_v1.*view. If not, add amart_<feature>view first; regenerate + applyapi_v1; rerunnpm run api:types. - 5.x.2 Implement the route in
atlas-frontend/app/<path>/page.tsxagainstlib/api.tswith typed responses fromapi-types.ts. - 5.x.3 Confirm the catalog at
/datashows the new endpoint without code changes (the introspection-driven render handles it). - 5.x.4 Add a smoke check (curl + grep on rendered HTML, or a Playwright test if testing infra lands).
Done when
This phase has no "complete" condition — it's continuous. Treat it as the steady-state activity that follows the PLAN, not a phase to close out.
Phase 6: Document both apps in setup.md + cross-references
Update contributor docs to reflect the split.
Tasks
- 6.1 Update
website/docs/contributors/setup.mdto describe two Next.js apps:atlas-contributor-frontend/— direct Postgres, what contributors run during ingestion verification.atlas-frontend/— PostgREST consumer, the customer-facing app, ports 3001 in dev.- Reference each app's own README for fork/extend.
- 6.2 Update
website/docs/contributors/index.md"Where things live" table to list both apps. - 6.3 Update
website/docs/contributors/data-journey.mdStage 8 — clarify that the customer app dogfoods the API and that the contributor app keeps direct-DB access for verification work. - 6.4 Update
CLAUDE.md"Key Folders" section. - 6.5 Update
atlas-data/CONTRIBUTING.mdif it references the frontend path. - 6.6 Scaffold a stub for the external developer docs surface at
website/docs/developers/index.md. Single page with:- One paragraph framing the audience (people consuming
api-atlas.helpers.noto build their own apps — frontend, CLI, agent, mobile, scripts). - A pointer to the customer app's README at
atlas-frontend/as the canonical fork-me reference. - A pointer to the live Swagger 2.0 spec at
api-atlas.helpers.no/for the schema. - A "content TBD" note pointing at INVESTIGATE-developer-docs-surface.md for the planned full developer docs (getting started, API reference rendering, versioning policy, examples).
- No content beyond the stub — full developer docs are a separate effort, deliberately out of scope for PLAN-005.
- One paragraph framing the audience (people consuming
Validation
grep -rn "atlas-frontend\|atlas-contributor-frontend" website/ CLAUDE.md atlas-data/CONTRIBUTING.md
# Every match should refer to the correct app for its context (customer-facing for atlas-frontend; contributor-facing for atlas-contributor-frontend).
Done when
- All contributor docs distinguish the two apps clearly.
- A new contributor reading setup.md cold knows which app to run for what purpose.
Acceptance Criteria
-
git log --diff-filter=R --summaryshows the renameatlas-frontend → atlas-contributor-frontend. - Fresh
atlas-frontend/exists with zero DB driver dependencies. -
atlas-frontend/src/lib/api.ts+api-types.tsexist;npm run api:typesregenerates types from live PostgREST. -
/datacatalog atlocalhost:3001/datalists everyapi_v1.*endpoint with row count and descriptions, sourced from the live OpenAPI spec, with no hardcoded endpoint names in the page source. -
app/page.tsx(homepage) renders and links to/data. - setup.md, index.md, data-journey.md, CLAUDE.md describe the two apps correctly.
- No literal
atlas-frontend/reference in the repo points at the wrong app for its context. -
website/docs/developers/index.mdexists as a stub pointing at the customer app's README + the live OpenAPI spec, with a forward reference toINVESTIGATE-developer-docs-surface.md.
(Detail pages, any new mart_* views, and the actual developer-docs content are out of PLAN-005's scope — Phase 5 scale-out and the developer-docs INVESTIGATE handle them.)
Files to Modify
Renamed (whole folder):
atlas-frontend/→atlas-contributor-frontend/
New (created in Phase 2+):
atlas-frontend/— fresh Next.js scaffoldatlas-frontend/src/lib/api.ts,atlas-frontend/src/lib/api-types.tsatlas-frontend/app/page.tsx— homepage linking to/dataatlas-frontend/app/data/page.tsx— introspection-driven catalogatlas-frontend/.env.exampleatlas-frontend/README.md— forkable framing
New (Phase 5, scale-out — not part of PLAN-005's acceptance):
- Detail-page route files (
app/coverage-gap/barnefattigdom/page.tsx,app/kommuner/[kommune_nr]/page.tsx, etc.) - New
mart_*views inatlas-data/dbt/models/marts/api/when a route needs data not inapi_v1.*today (withschema.ymldescriptions; regenerate + applyapi_v1)
Updated docs:
website/docs/contributors/setup.mdwebsite/docs/contributors/index.mdwebsite/docs/contributors/data-journey.mdCLAUDE.mdatlas-data/CONTRIBUTING.md(if it references the frontend)- Any other markdown matching
grep -n "atlas-frontend"
New (stub for external developer docs):
website/docs/developers/index.md— placeholder; real content tracked byINVESTIGATE-developer-docs-surface.md
Out of Scope
- The contributor app's specific feature set (FK integrity dashboards beyond what's already there, dbt test result surfacing, etc.) — separate design conversation, see "Out of scope" in the INVESTIGATE.
- Production deploy of
atlas.helpers.no— covered by INVESTIGATE-deployment-pipeline.md. - Production deploy of
developer-atlas.helpers.no(Docusaurus) — same. - Auth / JWT for the contributor app's optional staging URL — separate plan.
- Migrating the customer app to call PostgREST through an API gateway (Gravitee/APIM) — v1.5+ insertion, not a v1 concern.
Cross-references
- INVESTIGATE-frontend-data-access-architecture.md — the architectural commitments this PLAN executes.
- INVESTIGATE-public-api-surface.md — the parent investigation; this PLAN is the realisation of its [Q18] (PLAN-E).
- PLAN-004-postgrest-api-v1-wrapper.md — the api_v1 wrapper layer + auto-generator + 5 gates that this PLAN consumes.
talk2.md— cross-repo coordination thread that captured the PostgREST integration verification.atlas-frontend/— the codebase that gets renamed in Phase 1.website/docs/contributors/setup.md— gets updated in Phase 6.
Implementation Notes
- Order matters within Phase 1. Do the
git mvbefore updating doc references — references update against the new path. If you update docs first, the path doesn't exist yet and the reviewer flags broken links. - Phase 2 + Phase 3 are blocking for Phase 4. Don't try to write the catalog page before the API client exists.
- The catalog page must stay introspection-driven. Do not hardcode endpoint names in
app/data/page.tsx— fetch them from PostgREST's spec at request time. The validation grep enforces this. The whole point of the catalog as the first route is that it self-updates asapi_v1.*evolves. - Phase 5 is open-ended on purpose. Don't artificially close it. Treat each new customer route as its own small PR. Atlas-data coupling appears only when a route needs data not yet in
api_v1.*; themart_*view auto-generator from PLAN-004 makes it a one-line addition + regenerate, not a structural change. - Forkability discipline (Phase 2.5) is the single hardest convention to maintain. ESLint rule + a comment at the top of
lib/api.tsare the lightest enforcement; periodic review of the customer app's import graph catches drift.