Spec 0004 — mono-web migration gap analysis and plan
proposed migration contract. This document does not authorize a production cutover.
Status: proposed migration contract. This document does not authorize a production cutover.
Scope and counting rules
mw/meansmw; unprefixed paths mean this effx repository. The intended product is a modular backend, PostgreSQL, a generated HTTP client, and two browser apps (mw/docs/architecture.md,mw/docs/system.md).- The 25 bounded-context names come from every
BoundedContextdeclaration inmw/docs/model/contexts.cml(including six external contexts). This to-be map does not assert a standalone runtime module for each (mw/docs/model/contexts.cml,mw/docs/architecture.md). - An endpoint is one
HttpApiEndpoint.{get,post,put,patch,delete}declaration inmw/packages/http-api/src/*.ts. The external operation index lists 107;InternalReceiptsApiadds one outside external OpenAPI. There are 15 external groups and one internal group (mw/packages/sdk/native-api-operations.json,mw/packages/http-api/src/api.ts,mw/packages/http-api/src/receipts.ts). - A domain-service count means concrete
Context.Serviceclass declarations inmw/packages/domain/src/(including two Delivery ports and three non-context ports); it does not count service methods. A Foldkit-model count means schemas supplied to an independentRuntime.makeElement, including two inline Organization models (mw/packages/domain/src/runtime-services.ts,mw/packages/domain/src/notification/service.ts,mw/apps/dashboard/app/foldkit/organization/management.ts,mw/apps/dashboard/app/foldkit/organization/delegations.ts). - A DB-table count means distinct main PostgreSQL tables at the registered migration head, not the count of historical
CREATE TABLEstatements or a live database inspection. Deduplicate drops/recreations; include the migration ledger; exclude the separate Tutor proof schema. Assign each table once by the owner/subject rather than its filename prefix (mw/packages/database/src/{migrations,schema-bookkeeping}.ts,mw/packages/database/migrations/0073-retire-native-school-surveys.sql,mw/packages/database/migrations/0075-substitute-admission-outcome-coverage-records.sql). - Split mixed groups by endpoint owner:
directoryhas People 1 and Schools 3;onboardinghas Recruitment 2 and Identity 1;organizationhas Organization 12 and Mailing 1;systemhas Identity 6 and shared health 1 (mw/packages/http-api/src/{directory,onboarding,organization,system}.ts).
1. Inventory
| CML bounded context | HTTP endpoints (group allocation) | Domain services | Active PG tables | Foldkit models | Count sources |
|---|---|---|---|---|---|
| AccessControl | 0 | 1 | 12 | 0 | mw/packages/domain/src/authz/service-principal-grants.ts; mw/packages/database/migrations/{0023-declarative-authorization-rules,0076-reach-and-delegation}.sql |
| Identity | 7 (system 6, onboarding 1) | 1 | 21 | 0 | mw/packages/http-api/src/{system,onboarding}.ts; mw/packages/domain/src/identity/service.ts; mw/packages/database/migrations/{0015-native-identity-better-auth,0027-native-oauth-provider}.sql |
| People | 3 (profile 2, directory 1) | 1 | 8 | 1 | mw/packages/http-api/src/{profile,directory}.ts; mw/packages/domain/src/profile/service.ts; mw/packages/database/migrations/{0010-native-recruitment-applicant-assignment,0049-native-person-reconciliation}.sql; mw/apps/dashboard/app/foldkit/profile/main.ts |
| AcademicCalendar | 0 | 0 | 1 | 0 | mw/docs/model/contexts.cml; mw/packages/database/migrations/0002-admission-period-authority.sql (admission_period_semesters) |
| Organization | 12 (organization) | 1 | 14 | 3 | mw/packages/http-api/src/organization.ts; mw/packages/domain/src/organization/service.ts; mw/packages/database/migrations/{0008-organization-authority,0060-organization-access-lifecycle}.sql; mw/apps/dashboard/app/foldkit/organization/{main,management,delegations}.ts |
| Schools | 3 (directory) | 1 | 5 | 1 | mw/packages/http-api/src/directory.ts; mw/packages/domain/src/schools/service.ts; mw/packages/database/migrations/{0019-schools-directory,0061-scoped-school-administration}.sql; mw/apps/dashboard/app/foldkit/schools/main.ts |
| Admissions | 14 (admissions 10, admissionOutcomes 4) | 2 | 14 | 0 | mw/packages/http-api/src/{admissions,admission-outcomes}.ts; mw/packages/domain/src/{admissions/service,application/returning}.ts; mw/packages/database/migrations/{0002-admission-period-authority,0075-substitute-admission-outcome-coverage-records}.sql |
| Recruitment | 18 (recruitment 16, onboarding 2) | 1 | 26 | 4 | mw/packages/http-api/src/{recruitment,onboarding}.ts; mw/packages/domain/src/recruitment/service.ts; mw/packages/database/migrations/{0010-native-recruitment-applicant-assignment,0035-applicant-account-onboarding,0062-recruitment-maintenance}.sql; mw/apps/dashboard/app/foldkit/{recruitment,recruitment-maintenance,interview,scheduling}/main.ts |
| Placements | 16 (placements 10, certificates 6) | 1 | 27 | 1 | mw/packages/http-api/src/{placements,certificates}.ts; mw/packages/domain/src/placements/service.ts; mw/packages/database/migrations/{0032-native-volunteer-placement,0058-dated-school-service-commitment,0080-days-served-and-certificates}.sql; mw/apps/dashboard/app/foldkit/dated-school-service/main.ts |
| Economy | 14 (receipts 13, internal 1) | 3 | 9 | 0 | mw/packages/http-api/src/receipts.ts; mw/packages/domain/src/receipt/{service,auxiliary-service,file-service}.ts; mw/packages/database/migrations/{0001-receipt-authority,0048-expense-settlement-evidence}.sql |
| FileCustody | 0 | 0 | 0 | 0 | mw/docs/model/contexts.cml; receipt file service under mw/packages/domain/src/receipt/file-service.ts and mw/apps/backend/src/receipt/filesystem.ts |
| TeamApplications | 7 | 1 | 4 | 1 | mw/packages/http-api/src/team-application.ts; mw/packages/domain/src/team-application/service.ts; mw/packages/database/migrations/0069-team-applications.sql; mw/apps/dashboard/app/foldkit/team-applications/main.ts |
| SocialEvents | 3 | 1 | 3 | 1 | mw/packages/http-api/src/social-events.ts; mw/packages/domain/src/social-events/service.ts; mw/packages/database/migrations/0043-native-social-event-creation.sql; mw/apps/dashboard/app/foldkit/social-events/main.ts |
| Content | 8 | 2 | 5 | 1 | mw/packages/http-api/src/content.ts; mw/packages/domain/src/content/{service,content-service}.ts; mw/packages/database/migrations/0020-content-publication.sql; mw/apps/dashboard/app/foldkit/content/main.ts |
| Contact | 1 | 2 | 1 | 0 | mw/packages/http-api/src/contact.ts; mw/packages/domain/src/contact/service.ts; mw/packages/database/migrations/0030-native-contact-quota.sql |
| Mailing | 1 (organization.listMailingLists) | 0 | 0 | 0 | mw/packages/http-api/src/organization.ts; mw/packages/database/src/organization/mailing-lists-postgres.ts |
| Messaging | 0 | 0 | 0 | 0 | mw/docs/model/contexts.cml; Delivery ports in mw/packages/domain/src/mail.ts and mw/packages/domain/src/notification/service.ts |
| Delivery | 0 | 2 | 4 | 0 | mw/packages/domain/src/{mail,notification/service}.ts; mw/packages/database/migrations/{0034-password-recovery,0035-applicant-account-onboarding,0078-team-application-persisted-queue}.sql |
| Reporting | 0 | 0 | 0 | 0 | mw/docs/model/contexts.cml; projections in mw/packages/database/src/receipt/projections.ts |
| LegacySymfony | 0 | 0 | 0 | 0 | External context in mw/docs/model/contexts.cml; native source scope is mw/packages/http-api/src/api.ts |
| GoogleWorkspace | 0 | 0 | 0 | 0 | External context in mw/docs/model/contexts.cml; native source scope is mw/packages/http-api/src/api.ts |
| GoogleForms | 0 | 0 | 0 | 0 | External context in mw/docs/model/contexts.cml; six native survey tables retired in mw/packages/database/migrations/0073-retire-native-school-surveys.sql |
| Slack | 0 | 0 | 0 | 0 | External context in mw/docs/model/contexts.cml; native source scope is mw/packages/http-api/src/api.ts |
| MailProvider | 0 | 0 | 0 | 0 | External context in mw/docs/model/contexts.cml; native source scope is mw/packages/http-api/src/api.ts |
| HkDirCatalogue | 0 | 0 | 0 | 0 | External context in mw/docs/model/contexts.cml; native source scope is mw/packages/http-api/src/api.ts |
| CML subtotal (25) | 107 | 20 | 154 | 13 | mw/docs/model/contexts.cml; mw/packages/database/src/migrations.ts; mw/apps/dashboard/app/foldkit |
| Shared/unassigned | 1 (system.health) | 3 ports | 2 ledgers/HTTP receipts | 1 shell | mw/packages/http-api/src/system.ts; mw/packages/domain/src/{runtime-services,tutor/d1-proof}.ts; mw/packages/database/src/schema-bookkeeping.ts; mw/apps/dashboard/app/foldkit/dashboard/main.ts |
| Total | 108 = 107 external + 1 internal | 23 | 156 | 14 | mw/packages/http-api/src/api.ts; mw/packages/sdk/native-api-operations.json; mw/packages/database/src/migrations.ts; mw/apps/dashboard/app/foldkit |
The 156-table count includes native_http_idempotency_receipts and
vektorprogrammet_schema_migrations as global. It excludes the separate
Tutor D1 proof store (mw/packages/database/migrations/0029-native-http-semantics.sql,
mw/packages/database/src/schema-bookkeeping.ts,
mw/packages/database/migrations/tutor/0001-tutor-event-store.sql).
Tables are assigned once by the aggregate/subject, not filename prefix:
person_profiles starts in a recruitment migration but belongs to People;
organization delegations/grants are AccessControl facts; volunteer affiliation
belongs to Placements (mw/packages/database/migrations/0010-native-recruitment-applicant-assignment.sql,
mw/packages/database/migrations/0076-reach-and-delegation.sql,
mw/packages/database/migrations/0032-native-volunteer-placement.sql,
mw/docs/model/contexts.cml).
Better Auth's /api/auth/* is outside the 108 native routes
(mw/packages/http-api/src/api.ts, mw/apps/backend/src/router.ts).
Contract commonality
The operation-index split below is an exact count, not a guess from file sizes.
The group key is the OpenAPI operation-id prefix.
directory, onboarding, organization, and system cross CML ownership boundaries.
The internal evidence endpoint is not in the external index
(mw/packages/sdk/native-api-operations.json,
mw/packages/sdk/scripts/generate-operations.ts,
mw/packages/http-api/src/api.ts).
| External group | Operations | CML allocation | Contract file |
|---|---|---|---|
admissionOutcomes | 4 | Admissions | mw/packages/http-api/src/admission-outcomes.ts |
admissions | 10 | Admissions | mw/packages/http-api/src/admissions.ts |
certificates | 6 | Placements | mw/packages/http-api/src/certificates.ts |
contact | 1 | Contact | mw/packages/http-api/src/contact.ts |
content | 8 | Content | mw/packages/http-api/src/content.ts |
directory | 4 | People 1; Schools 3 | mw/packages/http-api/src/directory.ts |
onboarding | 3 | Recruitment 2; Identity 1 | mw/packages/http-api/src/onboarding.ts |
organization | 13 | Organization 12; Mailing 1 | mw/packages/http-api/src/organization.ts |
placements | 10 | Placements | mw/packages/http-api/src/placements.ts |
profile | 2 | People | mw/packages/http-api/src/profile.ts |
receipts | 13 | Economy | mw/packages/http-api/src/receipts.ts |
recruitment | 16 | Recruitment | mw/packages/http-api/src/recruitment.ts |
social-events | 3 | SocialEvents | mw/packages/http-api/src/social-events.ts |
system | 7 | Identity 6; shared health 1 | mw/packages/http-api/src/system.ts |
team-applications | 7 | TeamApplications | mw/packages/http-api/src/team-application.ts |
| External total | 107 | 15 groups | mw/packages/sdk/native-api-operations.json |
internal | 1 | Economy evidence only | mw/packages/http-api/src/receipts.ts, mw/packages/http-api/src/api.ts |
Examples of what the DB-table counts do not mean:
- The 14 Organization-owned tables exclude grant/delegation and volunteer
affiliation facts attributed to AccessControl and Placements. Consumers do
not take ownership of the Organization SQL (
mw/packages/database/migrations/0008-organization-authority.sql,mw/packages/database/migrations/0032-native-volunteer-placement.sql,mw/packages/database/migrations/0076-reach-and-delegation.sql,mw/docs/model/contexts.cml). - The Recruitment footprint includes interview tables and applicant-account
invitation tables. The claim operation belongs to Identity, even though
the invitation originates in Recruitment (
mw/packages/database/migrations/0035-applicant-account-onboarding.sql,mw/docs/model/contexts.cml,mw/packages/http-api/src/onboarding.ts). - Economy has three domain-service declarations, while its 13 public endpoints
occupy one HttpApi group; a one-group-to-one-service migration is wrong
(
mw/packages/domain/src/receipt/service.ts,mw/packages/domain/src/receipt/auxiliary-service.ts,mw/packages/domain/src/receipt/file-service.ts,mw/packages/http-api/src/receipts.ts). - The 14 Foldkit programs count three Organization editor programs and one
shared dashboard shell. A CML context with no Foldkit program can still have
a React Router page (
mw/apps/dashboard/app/foldkit/organization/{main,management,delegations}.ts,mw/apps/dashboard/app/foldkit/dashboard/main.ts,mw/apps/dashboard/app/routes/dashboard.utlegg._index.tsx).
| Every endpoint or subset | Observed pattern | Source |
|---|---|---|
| 108/108 | One annotateAccessSpec each: exposure, accepted credential, principal kind, capability expression, named requirements, scope resolver, concealment, and SnapshotRead or Transaction decision time. | mw/packages/http-api/src/access.ts, mw/packages/http-api/src/{profile,receipts,social-events,system}.ts |
| 108/108 | Root RequestSchemaErrorMiddleware; 107 external, one internal. Each endpoint declares a success schema and typed problem responses. | mw/packages/http-api/src/api.ts, mw/packages/http-api/src/http-semantics.ts, mw/packages/http-api/src/endpoint-problems.ts |
| 94/108 explicit security middleware | 81 PersonSecurity, seven SessionSecurity, four invitation capability, one person-or-service, one Contact SSR. Fourteen endpoints have no explicit security marker, not no AccessSpec. | mw/packages/http-api/src/common.ts, mw/packages/http-api/src/contact.ts, mw/packages/http-api/src/{system,recruitment,receipts}.ts |
| Protected writes | A middleware marker does not grant authority. Resolve current credentials/facts and authorize inside the committing transaction, with a fresh check after any retry. | mw/docs/architecture.md, mw/apps/backend/src/profile/http.ts, mw/packages/database/src/organization/authority-postgres.ts |
| Problem wire format | NativeProblemRegistry owns the closed RFC 9457 code/status/body; problemUnion and endpointProblemResponses derive endpoint-specific status responses. Middleware adds credential problems; ProblemBoundaryLive is the only Cause consumer. | mw/packages/http-api/src/http-semantics.ts, mw/packages/http-api/src/endpoint-problems.ts, mw/packages/http-api/src/common.ts, mw/.agents/skills/effect-house/SKILL.md |
| Replayable commands | Headers, conditional ETags, HTTP response receipt, and domain command receipt are separate. Keep both receipts with facts in the transaction; provider work follows commit. | mw/packages/http-api/src/http-semantics.ts, mw/apps/backend/src/http-api/receipt-transaction.ts, mw/packages/database/src/profile/postgres.ts, mw/docs/architecture.md |
2. Pattern mapping
today means implemented and usable by effx now. gap names an extension that must land first. out-of-scope means the rewrite keeps the owner-authored behavior. A generated TypeScript declaration is never proof of runtime enforcement (AGENTS.md, packages/compiler/src/generate/http.ts, docs/decisions/0007-cedar-authorizes-effx-issues-leases.md).
| mono-web pattern/site | effx replacement | Status | Boundary preserved |
|---|---|---|---|
HttpApiEndpoint.get/post/... with simple input/success: mw/packages/http-api/src/profile.ts | @Query/@Command + @Http.Get/Patch, Operation + Exposure{http} → http.ts | today | effx currently emits one operations group with simple path strings, payload and success/errors (examples/users/src/operations.ts, packages/compiler/src/generate/http.ts). |
Group-specific query, params, payload media, headers, status, cache/304/ETag, CORS: mw/packages/http-api/src/{profile,receipts}.ts | @Http.Contract(...) + HttpContract extension node and richer HTTP generator | gap | Do not turn conditional read or replay into a plain 200 response (mw/packages/http-api/src/http-semantics.ts, packages/compiler/src/generate/http.ts). |
HttpApiBuilder.group plus raw Web handler: mw/apps/backend/src/profile/http.ts | Generated ApiHandlers delegates to domain Effect handler; boundary adapter remains for Web response/receipt cases | gap | handleRaw, request decoding, and transaction composition stay explicit until the contract extension can express them (packages/compiler/src/generate/http.ts, mw/apps/backend/src/profile/http.ts). |
Domain Context.Service: mw/packages/domain/src/profile/service.ts | Handler requirements inferred to Service nodes and Requires edges | today | Service interface, live Layer, policy, complete commands remain hand-written (packages/compiler/src/extensions/core.ts, mw/packages/database/src/profile/postgres-layer.ts). |
Generated SDK operation: mw/packages/sdk/src/effect-client.ts | effx generated client.ts (HttpApiClient over generated API) | today, parity gap | effx lacks mono-web cookie/bearer/header transform, public EffectSdkFailure, external-only operation index and bridge compatibility (packages/compiler/src/generate/client.ts, mw/packages/sdk/scripts/generate-operations.ts). |
Foldkit Command.define and messages: mw/apps/dashboard/app/foldkit/profile/command.ts | Opt-in @Foldkit.Command extension → command/message adapter from HTTP client operation | gap | Validation, Model, update, view, request identity, error wording remain owned by UI (mw/apps/dashboard/app/foldkit/profile/{model,update,view}.ts, packages/compiler/src/generate/client.ts). |
Effect Model.Class Select/Insert/Update/Json: mw/packages/domain/src/profile/schema.ts | @PersistentModel → Model/Focus/PersistsAs/ViewOf IR | today (metadata only) | Preserve Effect Model variants and explicit public projection, never emit partial PATCH from Model.update (packages/compiler/src/extensions/core.ts, mw/docs/architecture.md). |
SqlSchema.findAll/findOneOption and hand-written projection: mw/packages/database/src/receipt/projections.ts | @PersistentModel-linked SqlProjection extension for schema refs and codecs | gap | SQL joins, row locks, filters, ordering, snapshot time and transaction are adapter code (mw/packages/database/src/team-application/postgres.ts, mw/docs/architecture.md). |
Current authority check inside transaction: mw/apps/backend/src/profile/http.ts | @Authorize emits Capability/AuthorizedBy only today; @Access extension + runtime guard adapter | gap | The handler/database transaction, current relationship facts, credential evidence and lock order still decide, not TypeScript metadata (mw/packages/http-api/src/access.ts, mw/docs/architecture.md, docs/decisions/0007-cedar-authorizes-effx-issues-leases.md). |
Typed problem registry and endpoint union: mw/packages/http-api/src/http-semantics.ts, mw/packages/http-api/src/endpoint-problems.ts | @Errors asserts inferred handler E; separate @Http.Problems/ProblemContract projection | gap | Wire code/status/body, per-status headers, credential problems, translation and Cause boundary need registry-backed adapter (packages/compiler/src/extensions/core.ts, mw/apps/backend/src/http-api/problem.ts). |
Outbox insert/claim/settle: mw/packages/database/src/team-application/outbox.ts | Optional OutboxEffect IR links command to declared envelope/worker; no generic generated lifecycle | out-of-scope | Domain transaction writes immutable envelope; worker owns fencing, ordering, retry, quarantine, provider I/O (mw/packages/database/src/outbox-lifecycle.ts, mw/docs/architecture.md). |
Public OpenAPI: mw/packages/http-api/src/api.ts, mw/packages/sdk/scripts/generate-operations.ts | OpenAPI from generated HttpApi plus @Http.Metadata projection | gap | Preserve external/internal split, group tags, security schemes, operation IDs and provenance (packages/compiler/src/generate/http.ts, mw/packages/http-api/src/access.ts). |
3. Ranked gaps and extension contracts
The site count is reach, not estimated removed lines. HTTP numbers count operations; SQL and Foldkit numbers count syntactic call sites/programs. Estimates are planning ranges for implementation plus focused tests, not measured effort (mw/packages/http-api/src, mw/packages/database/src, mw/apps/dashboard/app/foldkit).
| Rank; reach | Proposed source annotation → IR → generator | Size estimate | Primary evidence |
|---|---|---|---|
| 1; 108 HTTP endpoints | @Http.Contract({ group, params, query, headers, payload, success, errors, status, mediaType, cache, conditional }) → Extension node HttpContract linked to Exposure/Operation; expand http.ts to honor HttpApiSchema.WithHeaders, typed params, alternate statuses and raw-response escape. Reject unsupported combinations. | L (5–8 files, ~500–900 LOC) | mw/packages/http-api/src/{api,profile,receipts,http-semantics}.ts; packages/compiler/src/extensions/transports.ts; packages/compiler/src/generate/http.ts |
| 2; 108 AccessSpecs, 94 explicit security markers | @Access(spec)/@Http.Middleware(marker) → AccessContract extension node with credential set, principal kinds, capability expression, requirements, scope resolver, concealment and decision time. Generate middleware declaration and a required guard hook. Fail compilation if a protected generated endpoint has no guard binding. | L (5–8 files, ~450–850 LOC) | mw/packages/http-api/src/{access,common,api}.ts; mw/apps/backend/src/profile/http.ts; packages/compiler/src/extensions/core.ts |
| 3; 108 typed endpoint error boundaries | Keep inferred @Errors for domain E; add @Http.Problems(registry, codes) → ProblemContract extension node with code→status/schema/header refs and error mapping. Generate the endpoint's error union and boundary mapper from one registry. Check inferred E plus middleware failures for unmapped/undeclared codes. | L (4–7 files, ~450–800 LOC) | mw/packages/http-api/src/{http-semantics,endpoint-problems,common}.ts; mw/apps/backend/src/http-api/problem.ts; packages/compiler/src/extensions/core.ts |
| 4; 107 external SDK operations | Extend client.ts with generated group names and typed operation signatures, header/query/path/media inputs, options-driven cookie or bearer/fetch transform, failure type, external operation index and internal exclusion. Compile SDK callsites against generated types; do not invent a second operation list. | M–L (3–6 files, ~300–650 LOC) | mw/packages/sdk/src/{effect-client,promise,config}.ts; mw/packages/sdk/scripts/generate-operations.ts; mw/apps/dashboard/app/routes/__foldkit.profile.ts; packages/compiler/src/generate/client.ts |
5; persistent state across context families, 39 SqlSchema constructor calls in nine database files | Extend @PersistentModel({ table, views }) via SqlProjection node (select/insert/update codecs and named projections); generate typed codec helpers using the existing Effect Model and SqlSchema. Do not generate table migrations or infer SQL joins/business commands. | M (3–5 files, ~250–500 LOC) | mw/packages/domain/src/profile/schema.ts; mw/packages/database/src/{receipt/projections,team-application/postgres,schools/administration}.ts; packages/compiler/src/extensions/core.ts |
6; 29 Command.define sites, 14 Foldkit programs | @Foldkit.Command({ operation, successMessage, failureMessage }) → UiCommand extension node referring to operation/client and message schemas; foldkit.ts emits command execution with typed success/failure mapping. Keep Model, update/view, validation and bridge routing hand-written. | M (3–5 files, ~250–450 LOC) | mw/apps/dashboard/app/foldkit/{profile,social-events}/command.ts; mw/apps/dashboard/app/foldkit/profile/{model,update}.ts; packages/compiler/src/generate/client.ts |
| 7; 107 external operations, 15 external groups | @Http.Metadata on operation/group → OpenApiMetadata extension node; generate OpenApi.annotations with stable operationId, tags, security, x-vektorprogrammet-provenance, explicit external/internal roots and index from generated API. | M (3–5 files, ~200–450 LOC) | mw/packages/http-api/src/{api,access,common}.ts; mw/packages/sdk/scripts/generate-operations.ts; packages/compiler/src/generate/http.ts |
| 8; several outbox families and workers | No lifecycle generator in this migration. Optional @OutboxEffect and OutboxEffect node only after one envelope/claim-fence contract is proven; no generator for provider-specific recovery. | deferred | mw/packages/database/src/{outbox-lifecycle,team-application/outbox,receipt/outbox}.ts; mw/docs/architecture.md |
Extension constraints and acceptance data
These are compiler contracts, not runtime decorator implementations.
The existing Extension node has JSON data but no generic graph edge that
attaches metadata to an operation. A string convention inside JSON would hide
that relationship from graph analysis (packages/ir/src/Node.ts,
packages/ir/src/Edge.ts, docs/specs/0001-compiler-kernel.md).
| Invariant | Named change | Falsifier/source |
|---|---|---|
| Metadata is attached by a typed edge | Add ExtensionOf (extension → operation/group/root), bump IR format with explicit v1→v2 migration | Orphan metadata is rejected rather than silently ignored (packages/ir/src/{Edge,ApplicationIR}.ts; docs/decisions/0002-ir-is-graph-shaped-and-schema-defined.md). |
| Schemas are live | HttpContract stores SchemaRef, never a serialized schema AST | A transformed ProfileMergePatch survives generation (packages/ir/src/Node.ts, mw/packages/http-api/src/v2-schemas.ts, docs/decisions/0004-schemas-preserved-by-symbol-reference.md). |
| Source forms agree | Decorator and builder lower to the same namespaced annotation and normalized extension data | Equivalent declarations get identical semantic hashes (docs/specs/0002-source-frontend.md, docs/decisions/0003-canonical-json-is-a-projection.md). |
| Generated behavior is inspectable | Each generator emits ordinary Effect HttpApi, SDK or Command, not a hidden effx runtime | A generated protected endpoint with no bound guard fails compilation (docs/decisions/0001-aot-compilation-is-authoritative.md, docs/decisions/0008-generated-code-is-ordinary-effect.md). |
Transport failure is distinct from domain E | The problem projection combines declared registry codes, middleware errors, and an explicit mapping from inferred domain errors | Credential denial cannot become an undeclared 500 (mw/packages/http-api/src/common.ts, mw/packages/http-api/src/http-semantics.ts, packages/compiler/src/extensions/core.ts). |
| No two operation catalogs | Derive OpenAPI and the SDK index from the same generated HttpApi root | 107 external operations and the internal-only evidence route never swap roots (mw/packages/http-api/src/api.ts, mw/packages/sdk/scripts/generate-operations.ts). |
Required HttpContract fields for the first slice:
rootandgroup: external Profile root/group, not the defaultoperationsgroup (mw/packages/http-api/src/api.ts,packages/compiler/src/generate/http.ts).request: separateparams,query,headers, andpayloadschema references, plus accepted media type (mw/packages/http-api/src/profile.ts,packages/compiler/src/generate/http.ts).response: schema, status, response-header schema, content type, and conditional response variant (mw/packages/http-api/src/http-semantics.ts,mw/packages/http-api/src/profile.ts).middleware: reference to a contract marker, not an instruction to instantiate an identity service (mw/packages/http-api/src/common.ts,mw/apps/backend/src/http-api/transport.ts).metadata: summary, description, stable operation ID, tags, root-local exclusion, security and provenance (mw/packages/http-api/src/common.ts,mw/packages/http-api/src/api.ts).
Required AccessContract fields for the first slice:
- Credentials: cookie and delegated OAuth bearer are alternatives, never two
simultaneous principals (
mw/packages/http-api/src/common.ts,mw/packages/http-api/src/access.ts). - Principal and capability: Person with
profile.read-selforprofile.update-self, not the Foldkit navigation role (mw/packages/http-api/src/profile.ts,mw/docs/architecture.md). - Resource selection:
profile.current-person; required relationship:profile.owner(mw/packages/http-api/src/profile.ts). - Read decision time:
SnapshotRead; write decision time:Transaction. Never silently lower the second into the first (mw/packages/http-api/src/profile.ts,mw/docs/architecture.md). - Concealment: preserve the declared Reveal policy and account for the
separate credential-problem response (
mw/packages/http-api/src/access.ts,mw/packages/http-api/src/common.ts).
Required ProblemContract fields for the first slice:
- Registry reference, code set and resulting per-status response schemas
(
mw/packages/http-api/src/http-semantics.ts,mw/packages/http-api/src/endpoint-problems.ts). - Domain-error → problem-code mapping; an unmapped typed error is a build
diagnostic, not an implicit public body (
mw/apps/backend/src/profile/http.ts,packages/compiler/src/extensions/core.ts). - Credential challenge and
no-storeheaders; strict validation extensions are part of the wire schema (mw/packages/http-api/src/http-semantics.ts,mw/packages/http-api/src/common.ts). - Source evidence for the Profile read and update unions remains the one
registry and its two endpoint code lists
(
mw/packages/http-api/src/endpoint-problems.ts:91-126,mw/packages/http-api/src/http-semantics.ts).
Authority decision for mono-web
Effx ADR 0007 records future Cedar authorization, request-scoped leases and a compile-time Cedar schema. The current compiler only records capability and focus identities; its @Authorize does not check a request (docs/decisions/0007-cedar-authorizes-effx-issues-leases.md, packages/compiler/src/extensions/core.ts, packages/compiler/src/generate/http.ts).
Mono-web instead already has a relationship-fact interpreter, explicit AccessSpec, authority writers and a transaction-time rule. Moving a mutable write decision into a pre-handler Cedar lease would allow authority to change before commit; that breaks the current contract (mw/packages/domain/src/authz/reach.ts, mw/packages/http-api/src/access.ts, mw/packages/database/src/organization/authority-postgres.ts, mw/docs/architecture.md).
Recommendation: generate a declarative access contract and its required guard binding, but reuse mono-web's interpreter and database lock/transaction. Resolve the principal and current facts inside each committing transaction/retry; keep concealment and credential evidence in the HTTP adapter. A future Cedar backend can replace the interpreter only after it proves identical relationship facts, revocation, lock ordering and denial across the same PostgreSQL journey. Leases must not outlive the transaction or authorize an independent write (mw/apps/backend/src/profile/http.ts, mw/docs/model/contexts.cml, docs/decisions/0007-cedar-authorizes-effx-issues-leases.md).
4. Proving slice — People/Profile self-edit
The smallest authenticated browser + native HTTP + PostgreSQL + Foldkit context is People/Profile: two endpoints, one domain service, one Foldkit editor. Contact is smaller but anonymous and has no Foldkit editor. SocialEvents has three endpoints and a three-command screen. The synthetic effx User.Get/ChangeEmail example lacks mono-web's actual PostgreSQL, HTTP receipts and authority (mw/packages/http-api/src/{contact,profile,social-events}.ts, mw/apps/dashboard/app/foldkit/{profile,social-events}/command.ts, examples/users/src/services.ts).
GET /api/profile → profile.readOwnProfile PersonSecurity; SnapshotRead
PATCH /api/profile → profile.updateOwnProfile PersonSecurity; TransactionBoth endpoints accept cookie or delegated person OAuth bearer, use profile.current-person plus profile.owner, and declare endpoint-specific RFC 9457 problems. GET includes conditional read/ETag semantics; PATCH requires Idempotency-Key, If-Match, merge-patch media type and a response receipt (mw/packages/http-api/src/profile.ts, mw/packages/http-api/src/endpoint-problems.ts, mw/packages/http-api/src/common.ts).
| Seam | Exact files to migrate or keep | Decision |
|---|---|---|
| Declaration/projections | mw/packages/http-api/src/profile.ts, mw/packages/http-api/src/v2-schemas.ts, mw/packages/http-api/src/endpoint-problems.ts, mw/packages/http-api/src/api.ts; effx packages/compiler/src/{extensions/core.ts,extensions/transports.ts,generate/http.ts,generate/client.ts} | Generate the Profile contract/SDK from effx IR; keep the existing ProfileMergePatch schema and preserve other groups and roots. |
| Domain | mw/packages/domain/src/profile/{schema,service,errors}.ts | Keep Profile service, PersonProfile/PersonContactProfile models and domain errors. Do not generate partial update commands. |
| DB | mw/packages/database/src/profile/{postgres,postgres-layer}.ts; migration origins mw/packages/database/migrations/{0010-native-recruitment-applicant-assignment,0011-native-recruitment-interview-scheduling,0014-native-profile-self-edit,0029-native-http-semantics}.sql | Keep SQL joins/locks, two revision checks, command receipt, ETag authority revision. No migration rewrite. |
| Auth/runtime | mw/apps/backend/src/profile/http.ts, mw/apps/backend/src/main.ts, mw/apps/backend/src/router.ts, mw/packages/database/src/organization/authority-postgres.ts, mw/apps/backend/src/http-api/receipt-transaction.ts | Plug generated handler contract into existing identity, authorization and transaction boundary. |
| SDK/UI | mw/packages/sdk/src/{effect-client,promise}.ts, mw/packages/sdk/native-api-operations.json, mw/apps/dashboard/app/lib/api.server.ts, mw/apps/dashboard/app/routes/{dashboard.profile.rediger._index.tsx,__foldkit.profile.ts}, mw/apps/dashboard/app/foldkit/profile/{model,message,command,update,view,bridge,browser-client,main,elements}.ts, mw/apps/dashboard/app/foldkit/profile/styles.css | Preserve the SDK derivation from the canonical generated HttpApi. Replace duplicate wiring, not the browser proxy, Model, state transitions, styling or rendering. |
| Acceptance | mw/apps/dashboard/e2e/{native-profile-self-edit.spec.ts,run-real-native-profile-self-edit.mjs,native-profile-self-edit-seed.mjs}; mw/apps/backend/src/profile/http.test.ts; mw/packages/database/src/profile/postgres.test.ts; mw/packages/database/runtime/profile-postgres-proof-main.ts | Run the existing browser/API/PostgreSQL journey and two-connection proof against the effx-generated backend contract. |
The four Profile-owned tables are person_profiles, person_contact_profiles, profile_http_versions, and profile_self_edit_commands. The shared transport also writes native_http_idempotency_receipts. Authentication and scope additionally read Identity/Organization tables; those are dependencies, not Profile-owned tables (mw/packages/database/src/profile/postgres.ts, mw/packages/database/migrations/{0014-native-profile-self-edit,0029-native-http-semantics}.sql, mw/apps/backend/src/profile/http.ts).
Exact journey trace
The first slice keeps the same human task: edit your own name and contact
information; observe a fresh profile, not another person's record.
The named browser suite and runner already traverse these boundaries
(mw/apps/dashboard/e2e/native-profile-self-edit.spec.ts,
mw/apps/dashboard/e2e/run-real-native-profile-self-edit.mjs).
/login
→ Better Auth cookie, Identity account/session [Identity dependency]
→ dashboard.profile.rediger._index loader [server-side SDK read]
→ GET /api/profile [PersonSecurity, Profile read AccessSpec]
→ profile name/contact/version SQL snapshot [People-owned facts]
→ Foldkit Profile Model + view [local field and request state]
→ SaveProfile Command [one typed asynchronous effect]
→ PUT /profile [same-origin dashboard action, not native backend]
→ SDK profile.updateOwnProfile [PATCH /api/profile]
→ fresh authority + If-Match + idempotency inside transaction
→ profile rows + domain receipt + HTTP response receipt [one commit]
→ new Profile response + ETag → Foldkit success messageThe loader, native handler, transaction and Foldkit command edges come from
mw/apps/dashboard/app/routes/dashboard.profile.rediger._index.tsx,
mw/apps/backend/src/profile/http.ts,
mw/packages/database/src/profile/postgres.ts, and
mw/apps/dashboard/app/foldkit/profile/command.ts.
The browser action is mw/apps/dashboard/app/routes/__foldkit.profile.ts.
| Check retained from the existing journey | Boundary that fails if semantics drift |
|---|---|
| GET and PATCH without a credential return 401 problems | PersonSecurity and credential evidence (mw/apps/dashboard/e2e/native-profile-self-edit.spec.ts; mw/packages/http-api/src/common.ts) |
| The editor reads and changes the authenticated person's fields | SDK, browser action, Foldkit Model, Profile SQL (mw/apps/dashboard/e2e/native-profile-self-edit.spec.ts; mw/apps/dashboard/app/foldkit/profile/model.ts) |
| Unknown body fields and invalid media are rejected | Strict decoder and ProfileMergePatch (mw/apps/dashboard/e2e/native-profile-self-edit.spec.ts; mw/packages/http-api/src/v2-schemas.ts) |
| Stale ETag rejects a competing edit | Profile revision/HTTP version and If-Match (mw/apps/dashboard/e2e/native-profile-self-edit.spec.ts; mw/apps/backend/src/profile/http.ts) |
| Same idempotency identity replays, altered digest conflicts | Both domain and HTTP receipt stores (mw/apps/dashboard/e2e/native-profile-self-edit.spec.ts; mw/packages/database/src/profile/postgres.ts; mw/apps/backend/src/http-api/receipt-transaction.ts) |
| Another person cannot edit this person's profile | Current actor/scope inside transaction (mw/apps/dashboard/e2e/native-profile-self-edit.spec.ts; mw/apps/backend/src/profile/http.ts) |
| Browser form has no serious/critical axe violation | Retained human screen, not just generated backend (mw/apps/dashboard/e2e/native-profile-self-edit.spec.ts) |
The runner also invokes the database Profile PostgreSQL race/replay proof.
Keep that boundary in the acceptance run. Its path is
mw/packages/database/runtime/profile-postgres-proof-main.ts; the runner is
mw/apps/dashboard/e2e/run-real-native-profile-self-edit.mjs.
Do not report this suite as passing before it runs at the migration revision.
The accepted local Profile result is recorded in docs/research/profile-slice-evidence.md and the current STATE.md.
Definition of done: The existing just e2e profile journey (native-profile-self-edit.spec.ts) runs against the effx-generated Profile contract/handlers and generated SDK, with real Better Auth and disposable PostgreSQL. Keep its unauthenticated 401, successful edit, stale conflict, replay, strict wire contract, actor confinement and axe assertions; do not replace it with an SDK echo or compiler-only fixture (mw/AGENTS.md, mw/apps/dashboard/e2e/native-profile-self-edit.spec.ts, mw/apps/dashboard/e2e/run-real-native-profile-self-edit.mjs).
Record a measured maintained-source LOC delta at the exact baseline revision and slice commit.
The baseline is 2,355 physical lines (wc -l) across 18 Profile-scoped source files, including mw/apps/dashboard/app/foldkit/profile/elements.ts (docs/research/profile-baseline.md).
The earlier 2,288-line figure omitted that 67-line file.
Compare the same contract, handler, domain, database, UI and proxy responsibilities after migration, including added effx declarations and hand-written adapters.
Exclude tests, migration files, shared infrastructure and generated files.
Report (after − 2,355) and a separate generated-line count. Do not claim a reduction without that observation (mw/packages/http-api/src/profile.ts, mw/apps/backend/src/profile/http.ts, mw/packages/domain/src/profile/{schema,service,errors}.ts, mw/packages/database/src/profile/{postgres,postgres-layer}.ts, mw/apps/dashboard/app/foldkit/profile/*.ts, mw/apps/dashboard/app/routes/{dashboard.profile.rediger._index.tsx,__foldkit.profile.ts}, docs/specs/0003-cli-and-vertical-slice.md).
Local result (2026-10-03): Profile parity passed on clean, unlanded mono-web dd7a5f399df335b30e630b55586e924f820f73aa (unpublished) with effx 17374d6008bbad4894ce38cc64f18808ca8d96ae. The existing Chromium, backend, PostgreSQL, HTTP contract, SDK and full-check commands passed at that revision. Maintained Profile source is 2,514 lines, up 159 from the 2,355-line baseline. The two effx TypeScript files add 76 generated lines separately. This is local acceptance, not deployment, branch landing, or proof for other contexts (docs/research/profile-slice-evidence.md, STATE.md).
The line increase describes the result; it does not stop the migration. Evaluate codegen by the repeated declarations and bindings it removes, the legibility of the resulting interface, and journey parity. Native Effect<A, E, R> signatures already typecheck Effect channels. Schools directory and management are next; add compiler work only for a specific blocker found in that slice (mw/packages/http-api/src/profile.effx.ts, mw/apps/backend/src/profile/http.ts, docs/specs/0010-target-profiles-and-emit-modes.md, docs/research/profile-slice-evidence.md).
5. Sequence and gates
| Gate | Required proof before next gate | What mono-web keeps hand-written |
|---|---|---|
| 0. Freeze parity baseline | Record exact mono-web source revision, 108 endpoint inventory, 107 external SDK operations, Profile e2e observations, and 18-file, 2,355-line Profile baseline. No mono-web source is imported into effx; mono-web is read-only input to this plan (mw/packages/sdk/native-api-operations.json, mw/apps/dashboard/e2e/native-profile-self-edit.spec.ts, docs/specs/0003-cli-and-vertical-slice.md). | All native behavior, contracts, SDK and UI. |
| 1. HTTP/problem/authority extension | Rich endpoint schema and registry-derived errors compile. Protected operation cannot emit without a runtime guard binding. Golden typed errors, cookie/bearer, strict headers/media, cache, and OpenAPI groups match source (mw/packages/http-api/src/{api,common,http-semantics,endpoint-problems}.ts, packages/compiler/src/generate/http.ts). | Domain policy, SQL transactions, credentials engine, response receipt implementation. |
| 2. SDK/Foldkit wiring | Generated client preserves operation names, typed failures, credential transform and internal exclusion. Opt-in Foldkit command adapter has typed message mapping (mw/packages/sdk/src/effect-client.ts, mw/apps/dashboard/app/foldkit/profile/command.ts). | Profile database layer and SQL joins, audit/outbox, Foldkit Model/update/view, React Router proxy. |
| 3. Profile proving slice | just e2e profile, with denial, stale revision, replay and browser→backend→PostgreSQL boundary, passes unchanged in meaning; report LOC delta and exact-source evidence (mw/apps/dashboard/e2e/native-profile-self-edit.spec.ts, mw/AGENTS.md). | Other 106 endpoints, Better Auth, authorization interpreter, migrations, workers. |
| 4. Context-by-context | Move one full operational journey at a time: Schools directory/management → SocialEvents → Content → TeamApplications → Organization → Admissions → Recruitment/onboarding → Placements/certificates → Economy/receipts → Identity/system. After each context, diff generated API+SDK against the original operation index and run its existing named just e2e or just golden suite with PostgreSQL (mw/AGENTS.md, mw/packages/http-api/src/api.ts, mw/packages/sdk/native-api-operations.json). | Each unconverted context; specialized business state machines, SQL, audit, outbox, provider workers, UI state/view everywhere. |
| 5. Shared cutover | Only after all 107 external plus one internal operations preserve wire/security parity and all named journeys pass, remove obsolete handwritten endpoint declarations/SDK operation copies. Obtain operator authority before deployment, writer transfer, production data movement or legacy shutdown (mw/AGENTS.md, mw/docs/architecture.md). | Better Auth, migrations, PostgreSQL adapters, business authority, Foldkit Model/update/view and delivery workers unless separately justified. |
In mono-web, real PostgreSQL suites, browser suites, full check/lint/test and goldens are heavy jobs. Run them only through just measure --class <class> -- <command...>; the machine-wide lock admits one at a time. This plan runs none (mw/AGENTS.md#verification-and-resources). Effx must compile its own declarations and fixtures; it must not import mono-web source or make its compiler depend on @vektorprogrammet/* (AGENTS.md, docs/specs/0001-compiler-kernel.md, mw/docs/architecture.md).
6. Operator decisions and risks
- Security backend: recommend existing mono-web AccessSpec/interpreter for migration, not a simultaneous Cedar policy rewrite. Cedar/lease parity is a separate authorized gate (
mw/docs/architecture.md,docs/decisions/0007-cedar-authorizes-effx-issues-leases.md). - Wire compatibility: decide whether all v0.2 headers/statuses/problem bodies and hidden internal operation remain byte/status compatible. Default: yes, because replay/credential/conditional-read assertions rely on them (
mw/packages/http-api/src/http-semantics.ts,mw/apps/dashboard/e2e/native-profile-self-edit.spec.ts,mw/packages/http-api/src/api.ts). - Codegen evaluation: Profile maintained source increased by 159 lines when the new declarations and adapters were counted. Generated code is separate. Compare repeated declarations and bindings, legibility, and preserved journeys rather than treating a lower LOC as acceptance. Native
Effect<A, E, R>already checks service and error channels (docs/research/profile-slice-evidence.md:30-38,mw/packages/http-api/src/profile.effx.ts,mw/apps/backend/src/profile/http.ts). - Migrations and data:
@PersistentModelnames a table but does not generate its constraints, history, receipt or migration. Existing applied SQL files remain frozen (packages/compiler/src/extensions/core.ts,mw/packages/database/src/migration-registry.ts,mw/AGENTS.md). - UI boundary: generated clients do not replace the browser-side proxy, Foldkit state machine or authentication cookie handling; keep them until an end-to-end proof shows an exact replacement (
mw/apps/dashboard/app/routes/__foldkit.profile.ts,mw/apps/dashboard/app/foldkit/profile/{browser-client,update,view}.ts,packages/compiler/src/generate/client.ts). - Cutover authority: this is a plan, not permission to deploy, mutate credentials or retire the legacy writer. The operator controls those actions (
mw/AGENTS.md,mw/docs/architecture.md).
Spec 0003 handoff — CLI and vertical slice (2026-10-03, agent `slice3`)
Previous Page
Spec 0005 — HTTP contract extension (Gate 1, part A)
spec-frozen. This is the operator-approved first implementation gate from spec 0004. AccessSpec runtime enforcement is part B, not a claim made by these contract projections.