Spec 0023 — TeamApplications migration, Gate 4
Status: operator-approved for local implementation (2026-10-04). The approved scope is exactly seven current TeamApplications operations. Do not merge the pilot branch, land the integration branch, change providers or credentials, or deploy. Paths below are relative to the unlanded mono-web integration worktree at clean baseline fcc2f3e029b49c9bd0f0446c37c193416c362f50 (unpublished), unless prefixed with effx/. The operator’s mono-web main stays read-only. Prior Profile, Directory/Schools, SocialEvents and Content acceptance is a precondition, not proof of TeamApplications (effx/docs/specs/0004-monoweb-migration-plan.md:316-327; effx/docs/research/content-slice-evidence.md).
Outcome and scope
The existing public visitor can view a team's intake and submit a standalone application. A current nonsuspended team member can read its applications. Its current leader can revise intake and delete an application. Replace all seven handwritten TeamApplications endpoint declarations and their raw backend binding with one compiler-generated team-applications group and typed raw binding. Preserve the native HTTP, OpenAPI, generated SDK, domain, PostgreSQL, Better Auth, public form and Foldkit journey. Do not add an eighth operation or a new review/appointment state (packages/http-api/src/team-application.ts:179-382; docs/system.md:496-509).
This is one contract-and-handler migration. TeamApplications remains the one domain capability, and the staff editor remains one Foldkit Model. The homepage's anonymous form is a separate public React surface, not another Foldkit state machine. The service, its SQL adapter, the outbox envelope, the existing Effect PersistedQueue lease/retry pilot, worker triggers, and provider Layer stay hand-written. Generating an endpoint never grants team authority or changes notification delivery (packages/domain/src/team-application/service.ts:70-143; docs/architecture.md:285-307; effx/docs/specs/0004-monoweb-migration-plan.md:318-325).
Existing wire and authority contract
All seven are external operations already in the 107-operation SDK index. Keep the exact names, methods, paths, schemas, response headers, problem unions and declared AccessSpecs:
Operation (prefix team-applications.) | Method and path | Credential, authority and time | Success and request contract |
|---|---|---|---|
readTeamApplicationIntake | GET /api/teams/:teamId/application-intake | Anonymous; resolver team-applications.public-intake; SnapshotRead, one team. | 200 PublicTeamApplicationIntake, public no-store; unknown or inactive team is 404. |
listTeamApplicationIntakes | GET /api/team-application-intakes | Anonymous; resolver team-applications.public-intakes; SnapshotRead, all matching. | 200 bounded TeamApplicationIntakeListResponse, public no-store. |
submitTeamApplication | POST /api/teams/:teamId/applications | Anonymous; resolver team-applications.application-create; Transaction, one team. | Required Idempotency-Key, strict TeamApplicationInput JSON; 201 TeamApplicationConfirmation with strong ETag, origin-relative Location, no-store. |
listTeamApplications | GET /api/teams/:teamId/applications | PersonSecurity cookie or delegated Person OAuth bearer; capability team-applications.read; resolver team-applications.team-applications; SnapshotRead, all matching. | Optional bounded cursor query, 200 private TeamApplicationListResponse with canManage only as presentation; private no-store. |
readTeamApplication | GET /api/team-applications/:applicationId | Same Person credentials and read capability; resolver team-applications.application-by-id; SnapshotRead, exactly one. | 200 private TeamApplicationResource; private no-store. |
deleteTeamApplication | DELETE /api/team-applications/:applicationId | PersonSecurity; capability team-applications.manage; resolver team-applications.application-by-id; Transaction, exactly one. | Required Idempotency-Key; 204 No Content, no-store, no If-Match or response body. |
reviseTeamApplicationIntake | PATCH /api/teams/:teamId/application-intake | PersonSecurity; capability team-applications.manage; resolver team-applications.intake-by-team; Transaction, exactly one. | Required Idempotency-Key and strong If-Match; application/merge-patch+json; 200 TeamApplicationIntakeResource with new strong ETag, no-store. |
The three public operations use anonymousNativeAccess with None credential, Anonymous principal and no capability. Four staff operations use personNativeAccess with a Person principal and the endpoint-local capability above. Each endpoint keeps its own ordered closed problem union: TeamApplicationsReadIntakeProblem, TeamApplicationsListIntakesProblem, TeamApplicationsSubmitProblem, TeamApplicationsStaffReadProblem (shared by both staff GETs), TeamApplicationsDeleteProblem, and TeamApplicationsReviseIntakeProblem. The original union order and registry-derived statuses are the source of truth; do not replace them with a shared superset. Submission includes intake-closed and rate-limit failures. Intake revision includes missing/failed pr…
Staff PersonSecurity can answer 401 before an endpoint-specific problem, and external Origin ingress can reject even an anonymous operation before its handler. Preserve those transport responses without adding them to every endpoint union (packages/http-api/src/common.ts:57-107; apps/backend/src/router.ts:434-443).
Behavior and authority that stay with mono-web
- Public intake and submission. The team and department must be active. The team must accept applications, its optional deadline must be in the future, and a deliverable team or fallback department mailbox must exist. The public form limits all seven fields. Every submission, including an exact replay, consumes the public rate limit before body parsing. The native handler still caps submission bytes at 131,072, decodes strictly, and records a distinct submission with a new key as another application (
docs/system.md:496-505;apps/backend/src/team-application/http.ts:307-394). - Staff reads. A current nonsuspended team member reads only its team's private fields. Staff credential resolution, membership lookup and AccessSpec evaluation happen inside the same repeatable-read snapshot. The service resolves the current actor before the HTTP guard emits a result. A global administrator has no implicit TeamApplications access (
docs/system.md:507-509;apps/backend/src/team-application/http.ts:157-223,406-474). - Staff mutations and receipts. A current team leader alone deletes or revises intake. In each serializable transaction, resolve the current principal, lock and authorize the target, evaluate the operation AccessSpec, then derive the HTTP identity and inspect any stored response. A revoked leader cannot replay an earlier success. The command receipt, attributable history, business row, outbox envelopes, queue item and HTTP response receipt keep their original atomicity. A retry of SQL serialization uses the existing receipt executor; no generated retry or new lock is introduced (
packages/domain/src/team-application/service.ts:70-143;apps/backend/src/team-application/http.ts:476-651;docs/architecture.md:96-113,285-307). - Delete and intake rules. Deletion clears undelivered private notification fields and records history without contact data or free text. Intake change requires the observed team entity tag against the fresh locked revision. An omitted merge-patch field leaves it unchanged;
nullclears the deadline but cannot deleteacceptApplication. Preserve the 4,096-byte patch limit, 204 deletion, 200/new-ETag intake result and exact 409/412/428 problem behavior (docs/system.md:507-509;packages/http-api/src/team-application.ts:88-94,300-359;apps/backend/src/team-application/http.ts:542-651). - Delivery. The confirmation means receipt, not provider delivery. The stored envelope keeps its effect identity and private-field policy. The queue owns leases, attempts and backoff; the outbox owns immutable message content and outcome. Delivery occurs after commit and can retry, quarantine or recover after restart. The current worker, queue migration, PgBouncer proof and provider Layer do not move into effx (
docs/system.md:502-505;docs/architecture.md:285-307;docs/specs/infrastructure-ports.md:30-65;packages/domain/src/team-application/service.ts:132-142). - UI. Keep the public homepage form, confirmation and privacy behavior. Keep the dashboard chooser and keyed per-team custom element. Its one Foldkit Model owns page/detail request identity, the observed intake ETag, pending mutation and stable key for an ambiguous retry. It drops stale replies and clears private data after denial or session loss. Disposal remains with the element. The generated SDK is the only native client, and the built dashboard's same-origin
/apiproxy stays in place. StaffcanManageis display state, never a grant. There is no TeamApplications-specific__foldkitproxy route (apps/dashboard/app/foldkit/team-applications/{main,model,update,view,command,browser-client,elements}.ts; `apps/dashboard/app/routes/dashboard.teamsokn…
Proposed generated source and binding seam
Use the accepted Content migration pattern: one declaration-only team-application.effx.ts and an app-owned adapter that exports the exact existing SchemaRefs, problem codes, metadata/access annotators and native HTTP headers. Http.group({ root: ExternalNativeApi, group: "team-applications", defaults: ... }) holds only facts shared by all seven, such as exposure, registry and operation metadata. Put middleware: [], None/Anonymous and the public resolver explicitly on each of the three public operations. Put middleware: [PersonSecurity], Person credentials, exact read/manage capability and staff resolver on each staff operation. Derive native anonymousNativeAccess or personNativeAccess from the declarations; reject a missing or wrong public/s…
Each exported Operation.query or Operation.command uses .in(TeamApplicationsGroup).http.<verb>(exactPath).http.contract({...}).http.problems({...}).http.access({...}).declare(). Retain params and optional cursor, TeamApplicationInput, merge-patch media type, IdempotencyHeaders versus IdempotencyIfMatchHeaders, 201/204/200 statuses, response headers and six named ordered problem unions. These are design names, not claims of committed source. No new SDK operation, fallback credential, generic all-context problem union or extra drain endpoint is legal in this slice. After complete source checks, replace the root group's seven handwritten endpoints and all backend callsites in one clean cutover. Remove obsolete exports and aliases rather than kee…
The backend binds exactly seven generated raw aliases to the existing readIntake, listIntakes, submit, listApplications, readApplication, deleteApplication and reviseIntake programs. All seven generated guards receive the real current request/actor. Public guards invoke the existing anonymous authorization. Staff guards use the actor resolved by the existing read snapshot or write transaction; they do not pre-authorize from a Foldkit button or a stale preflight. Preserve the existing rate-limit-before-body order, actor-before-replay order, semantic HTTP digest/identity, unchanged byte/header capsule and typed problem mapper (apps/backend/src/team-application/http.ts:125-223,250-394,406-678; `apps/backend/src/http-api/receipt-transaction.t…
Unmerged pilot branch is evidence, not migration authority
Read-only diff git diff main...feat/team-application-queue-pilot-0926 from operator-owned mono-web main at f433ea90 to pilot tip 6a5ccdafe87d8cc0c076015209b8372bb587f147 (unpublished): 46 files, +2,790/−129 lines. It proposes operation grants, a service principal/ServiceSecurity transport and a bounded external drain handler. It adds POST /api/team-application-deliveries/drain as an eighth external operation while retaining the original seven routes. New authz modules and migration 0081-service-principal-operation-grants.sql supply grants; the existing PersistedQueue outbox/lease files and migrations 0078/0079 are not changed in this diff. Its new drain tests are evidence for a separate operation-grant/trigger decision, not approval to change t…
Implementation and acceptance gates (not yet claimed)
The current effx dist-artifacts/manifest.json and mono-web tools/effx-dist/manifest.json both pin f3c6bf8ae6205b9b60a449d01faed63744beb765 with identical CLI/runtime SHA-256 entries. effx/STATE.md has not accepted S5 status elision. Pack unchanged at f3c6bf8: the requested re-vendor/parity step is a no-op, as the operator confirmed; no new pack or fresh four-group check was run for this design. The last clean accepted mono-web fcc2f3e0 (unpublished) source has whole OpenAPI SHA-256 81e7b872178803a3c8bf6ccf86b96b5185ba560e9661b47e98d1d0d4e827b749 and whole SDK-index SHA-256 c4abcca1878171c84fd3c531c3fe5c93f92329818c59d4b73a8bf796bca8aad3, 107 external operations, seven TeamApplications IDs and one internal receipts operation excluded (effx/docs/resea… **Bodyless 204 probe (not seven-group acceptance):** The single-endpoint fixture mono-web/packages/http-api/test/team-application-no-content.fixture.effx.tsuses genuine EffectHttpApiSchema.NoContent(aSchema.Voidwith status 204), not a fabricated body. Vendored f3c6bf8 stricteffx checkand contract emit both exited 0 with onlyEFFX0001. The emitted WithHeaders(NoContent.pipe(status(204)), ...)and a one-groupOpenApi.fromApiresponse matched **every JSON-path value** of the original DELETE 204 response, including nocontentand required no-store/Vary headers. The first fixture-onlyEFFX1102` was a missing access concealment field; correcting that input removed the error. This probe does not establish full OpenAPI/SDK byte parity unt…
After approval, apply these gates serially under devenv shell, with each heavy command inside just measure --class <class> -- ... and one machine-wide heavy job at a time (mono-web/AGENTS.md#verification-and-resources):
- Repair first if an actual defect appears. Commit a focused failing HTTP/PostgreSQL/browser regression red on a clean source before a separate minimal repair. Record the error and its source. Do not invent a repair because the unmerged pilot proposes one. Route a compiler problem to
monoweb-gaponly with the actual diagnostic from all seven typed declarations and a minimal real fixture. Do not bypass strict access or hand-edit generated output. - One IR, all seven. Run frozen Bun install;
just effx-nativewith the existing four groups plus TeamApplications in both compiler project files; runeffx checkfor contract and raw handlers with--strict-access; require equal semantic hashes. Regenerate OpenAPI and SDK from mono-web's recipes. Assert exactly 107 external IDs, the same seventeam-applicationsIDs, and one excluded internal receipts ID. Compare complete OpenAPI and SDK files byte-for-byte against the clean acceptedfcc2f3e0 (unpublished)reference above. Investigate any difference down to response path/status/security/header/schema/problem projection. Byte differences are not accepted by a group-only or SDK-only comparison. - Focused source checks. Run
packages/http-api/test/native-api.test.tsplus new seven-operation declaration/access tests; the SDK index and client tests;apps/backend/src/team-application/http.test.ts;packages/domain/src/team-application/policy.test.ts; and the databaseservice.test.ts/delivery.test.tsPGlite suites. Run the existingdelivery-pgbouncer.test.tson disposable PostgreSQL through PgBouncer, andqueue-migration.test.tson PGlite and PostgreSQL. Keep dashboardteam-applications/{update,paths,oslo-time}.test.ts,apps/dashboard/app/lib/browser-api.test.ts, and homepage{public-team-application,team-directory}.test.ts. Prove public/staff guards, rate limiting on replay, exact 201 ETag/Location and bodyless 204, malformed… - The named whole journey. Run
just golden team-applicationon the generated contract/handlers and generated SDK. Its runner builds the dashboard and homepage, starts native HTTP, real Better Auth, disposable PostgreSQL and loopback mail, then drives one Chromium session through 17 ordered browser checkpoints with independent database snapshots. Keep the public intake/form/confirmation, exact replay and distinct submission, rejected/rate-limited cases, current member/leader scope and denials, stale intake, delete/private-field removal, pending/failed/recovered delivery and existing accessibility/keyboard checks. The current source rejects serious/critical WCAG 2A/AA axe findings and asserts Enter at sign-in; it does not claim complete Tab orde… - Adjacent and aggregate regression gates. Run
just e2e profile,just e2e schools,just e2e social-events, andjust e2e content-publicationseparately, then full serialjust check --concurrency=1andjust test --concurrency=1. All named journeys and full checks must pass on one clean committed mono-web revision after the seven-group cutover. Collect each command, exit, wall time, peak RSS, tested source revision and tracked-tree cleanliness from the measured ledger. A previous accepted context or an earlier partial test run does not certify the new revision. - Descriptive source accounting. Count physical
wc -lfor the same named contract, backend HTTP, domain/SQL, Foldkit/proxy and homepage-public-form responsibilities at baselinefcc2f3e0 (unpublished)and at the accepted slice commit. Include newly authored.effx.tsand adapters in maintained after-state. Count generated contract/handler TypeScript and whole OpenAPI/SDK projections separately. The line delta describes the cutover; it is not a rejection criterion. Record any unverified provider, production, whole-lost-response or pilot-drain boundary without claiming parity from another test.
Definition of done: the original just golden team-application journey and its relevant fault modes run through seven effx-generated TeamApplications endpoints/raw handlers, the canonical SDK, real Browser/Better Auth and disposable PostgreSQL, with unchanged exact external OpenAPI/SDK bytes and the four adjacent named browser journeys. Domain authority, SQL atomicity, Foldkit behavior, persisted delivery, worker triggers and provider abstractions remain owned by mono-web. Approval of this specification permits only the local migration work and its tests; it is not permission to merge the pilot, deploy, touch credentials or production data, land the integration branch, or transfer the writer.
Operator approval recorded. Effect’s own HttpApiSchema.NoContent is the approved success schema for the existing bodyless DELETE 204. It is not a placeholder payload. The full OpenAPI and SDK byte gate still applies before backend work; stop and report any 204 JSON-path difference before proceeding.
Spec 0022 — Persistence ports and the adapter conformance suite
spec-frozen 2026-10-04 (operator-approved from docs/research/effect-fullstack-composition.md §6). Implementation starts only after spec 0020 (Annotation.define) merges to main; until then this file i…
Spec 0024 — Declaration density: derived channels, defaults, naming and binding inference
approved and frozen 2026-10-04 (operator). Order of work: spec 0015 → generator-only S5 (monoweb-gap) → spec 0020 → this spec → spec 0019. Lands only after the built-ins are re-expressed with identic…