Development Stage
Development runs through assembly-repo/docker-compose.yml.
Rules:
-
.env.exampleis the committed stage source of truth. -
SANMOPIA_STAGE_ENV_FILEselects another env file. -
manifest.yamlpins the exact assembly, contract, backend, and frontend Git revisions. Runpnpm repositories:checkbefore integrated evidence: every sibling repository must match its full lowercase commit and have a clean worktree. The assembly pin may name only the current commit or its immediate parent so the pinning commit can validate itself. -
Application services must not declare Compose
ports. -
Traefik is the only HTTP entrypoint inside the Compose network.
-
Application routing must use Docker labels.
-
Routed services must define
traefik.http.services.*.loadbalancer.server.port. -
providers.docker.exposedbydefault=falsemust stay enabled. -
OSS services run inside the same stage: Strapi for CMS/admin authoring, Novu for notification workflow delivery, Restate for long-running business workflow state, SpiceDB for relationship authorization, OPA for law, status, and context policy decisions, self-hosted Supabase core services for Postgres/Auth/PostgREST/Realtime/Storage, Zed for SpiceDB schema bootstrap, and LocalStack only for Novu development storage.
-
Reservation, settlement, and member-withdrawal state transitions must use Restate orchestration for retry and idempotency; Supabase remains the persisted ledger, query model, and RLS layer.
-
Internal scheduled-job HTTP routes must be protected by
SANMOPIA_INTERNAL_JOB_TOKENand called from trusted stage jobs only. -
Supabase Cron settings must be configured with
pnpm supabase:cron:configure; do not inline backend URLs or internal tokens in migration SQL. -
Supabase application migrations are applied with
pnpm supabase:migrateafterdbis healthy. The assembly command delegates to the backend’s pinned Supabase CLI and usessupabase db push; officialsupabase_migrations.schema_migrationshistory is the only ledger. If an existing stage has Sanmopia tables but empty official history, the runner performs a one-timesupabase migration repairbaseline before pushing. Never restore a custompsqlfile loop or application migration ledger. -
Migration discovery reads
backend-repo/supabase/migrationsfrom the host, independent of Mutagen. Mutagen is still required for live backend source: after application code changes, runpnpm mutagen:flushand restart onlybackend-devbefore HTTP proof. Usepnpm --dir ../backend-repo supabase:db:push:stage -- --dry-runto verify that stage and repository migration histories converge without repairing history or applying migrations. The env equivalent isSANMOPIA_SUPABASE_DB_PUSH_DRY_RUN=true pnpm supabase:migrate. -
Stage smoke and scenario fixtures must register themselves in
sanmopia_test_data_batchesplussanmopia_test_data_references.SANMOPIA_TEST_DATA_DEFAULT_TTL_HOURSsets the default expiry.pnpm test-data:reapis a dry-run that fails when expired fixture batches remain in a reachable stage. Actual deletion requiresSANMOPIA_TEST_DATA_REAPER_CONFIRM=delete-expired-test-data pnpm test-data:reap:apply. Retry a failed batch with the same confirmation plus-- --retry-failed. Reservation-booking batches recover their verified root reference and derive child cleanup order from live Postgres foreign keys before deletion; source-script names and manually guessed table order are not cleanup authority. Bounded completion runners may expire and reap their own registered batch from anEXITtrap. The shared smoke wrapper must not replace caller traps and must delete each copied container temp directory immediately after its subprocess. This keeps test data cleanup explicit while preventing long-lived fixture rows from becoming migration debt. -
Supabase JWT keys in
.env.examplemust be signed bySANMOPIA_SUPABASE_JWT_SECRET.pnpm stage:checkverifies both theanonandservice_rolerole claims because Kong/PostgREST reject mismatched keys only at runtime. -
Runtime smoke for privacy automation uses
pnpm supabase:cron:smoke; stale withdrawn-token smoke usespnpm privacy:stale-token:smokewithSANMOPIA_WITHDRAWN_MEMBER_ACCESS_TOKEN. -
Mother-visible reservation charges are read from finalized Supabase snapshots through the financial lifecycle API. Restate owns workflow progress, but Supabase remains the source for frozen charge display and audit evidence. When service-calendar changes create
charge_reprice_requiredfacts, paid payment closeout must use a finalized charge snapshot that acknowledges the latest impact plan, policy fingerprint, and impact fact ids. When those changes createcaregiver_assignment_review_requiredfacts, the assignment optimizer must acknowledge the latest impact plan before running the solver for that booking. When they createdaily_report_selection_requiredfacts, daily-care-report print-data builds must acknowledge the same frozen impact plan before generating report output. -
Financial lifecycle command APIs advance service delivery, caregiver payout, branch settlement, HQ settlement, and settlement completion against Supabase lifecycle and ledger-line rows. Restate remains the workflow-state engine for long-running orchestration and retry around those commands.
-
Do not add OpenFGA while SpiceDB remains the Zanzibar/ReBAC engine for this migration. OPA is allowed because it complements SpiceDB instead of replacing relationship authorization.
-
Traefik must join the Docker socket group through
SANMOPIA_DOCKER_SOCKET_GID. -
Stage services must use
SANMOPIA_STAGE_APPARMOR_PROFILE=unconfinedin this local Docker stage so Traefik can open the Docker Unix socket and Nginx can spawn workers. -
Static images must normalize copied asset permissions during build; host checkout modes must not decide runtime readability.
-
Mutagen project config is generated from env by
scripts/render_mutagen_project.mjs. -
Mutagen forwards local HTTP to the Traefik container; Compose still exposes no host ports.
-
Mutagen binary installs must include
mutagen-agents.tar.gz; Docker endpoint sync and forward sessions need the agent bundle. -
Mutagen sync ignores repo metadata such as
.git,.fractalops, caches, env files, and generated project files. -
mutagen-composeis not the standard because upstream marks it deprecated in Mutagen v0.18. -
The backend process executes
/workspace/backend-repofrom thesanmopia-stage-backend-sourcenamed volume. Ifpnpm mutagen:listshows the backend session disconnected, the worktree can be green while the live API is stale. For a proof run, first restore Mutagen sync or refresh that source volume and restart onlysanmopia-stage-backend. -
docs-devis an explicitdocsCompose profile and is not part of normal Mutagen stage startup. Start it only for human review withdocker compose --env-file .env.example --profile docs up -d --no-deps docs-devand stop it after review. The service waits for both assembly and contract source sync, mounts contract source read-only, and startspnpm docs:devwithout Astro--forcedependency rebuilding.restart: "no"prevents a TypeDoc or OpenAPI configuration error from becoming a CPU-heavy crash loop.SANMOPIA_DOCS_CPU_LIMIT,SANMOPIA_DOCS_MEMORY_LIMIT, andSANMOPIA_DOCS_NODE_MAX_OLD_SPACE_MBbound host impact during long reviews. -
frontend-devwaits for frontend source, installs through pnpm, and runs Astro withSANMOPIA_ASTRO_DEV_HOSTplus its service-specific container port. -
frontend-devmountscontract_sourceat/workspace/contract-repo:roso Astro SSR can import the generated TypeScript client from the contract workspace without copying contract code into frontend source. -
Frontend build uses the Astro Node standalone adapter. Authenticated pages such as
/mother/bookingmust opt out of prerendering so request cookies and server-only backend URLs are read at runtime instead of freezing booking context during static generation. -
frontend-devreceives browser-facingPUBLIC_SANMOPIA_API_BASE_URLand server-onlySANMOPIA_BACKEND_INTERNAL_URL. SSR API calls must prefer the internal URL because*.sanmopia.localhosthostnames are routed by Traefik/Mutagen for browsers, not by Docker DNS inside the frontend container. -
SANMOPIA_MOTHER_BOOKING_ACCESS_TOKENis a dev-only SSR smoke input. Populate it fromSANMOPIA_STAGE_MOTHER_SESSION_PATHonly when recreatingfrontend-devfor booking smoke; never expose service-role keys to frontend. -
Backend OSS/provider endpoints come from env:
NOVU_API_URL,NOVU_SECRET_KEY,RESTATE_INGRESS_URL,RESTATE_ADMIN_URL,SPICEDB_ENDPOINT,SPICEDB_HTTP_URL,SPICEDB_PRESHARED_KEY,BILLING_CLIENT_MODE,BILLING_CLIENT_DEBUG_MODE,KILLBILL_USERNAME,KILLBILL_PASSWORD,KILLBILL_API_KEY,KILLBILL_API_SECRET,KILLBILL_WEBHOOK_SECRET,KILLBILL_CREATED_BY, andKILLBILL_API_URL. The API URL must include the/1.0/kbbase path. Stage usesBILLING_CLIENT_MODE=debugso booking workflow smoke can complete without calling the real Kill Bill API. In debug mode, backend composition must not require real Kill Bill credentials during startup; provider-only admin/webhook adapters receive local placeholder values and production still fails fast underBILLING_CLIENT_MODE=killbill. Production must useBILLING_CLIENT_MODE=killbillwith real Kill Bill secrets. -
backend-devruns FastAPI throughuv run uvicorn sanmopia_modernization.main:app; Traefik routes to the internal container port and Compose still publishes no host ports. -
backend-devwaits forpyproject.tomlbefore callinguv runbecause source arrives through Mutagen into a named volume. Do not replace this with host bind mounts. -
backend-devmountscontract_sourceread-only at/workspace/contract-repo. The backend’s uv path dependency imports the generatedsanmopia-python-contractspackage from that repository; do not copy generated transport models into backend source. -
backend-devgetsPYTHONPATHfromSANMOPIA_BACKEND_PYTHONPATHso the src-layout backend imports consistently under Uvicorn and tests. -
workflow-state-devis the Restate stage service. It exposes ingress8080, admin/UI9070, and fabric5122only on Compose networks; backend commands useRESTATE_INGRESS_URL=http://workflow-state-dev:8080. -
db,auth,rest,realtime,storage,imgproxy, andkongare the self-hosted Supabase core in stage. Backend useshttp://kong:8000andpostgresql://postgres:...@db:5432/postgreson the internal network. Frontend usesSANMOPIA_SUPABASE_PUBLIC_URLthrough Traefik/Mutagen. Compose still publishes no Supabase host port. -
Supabase Postgres binds
POSTGRES_USERfromSANMOPIA_POSTGRES_ADMIN_USER, whichpnpm stage:checkconstrains tosupabase_admin. Do not set it topostgres: the imagemigrate.shconnects assupabase_admin, runs init scripts, creates/demotes thepostgresrole, and then applies migrations. If a bad stage boot already initialized the volume withoutsupabase_admin, stop the stage and remove only the local Supabase stage volumes before starting again:sanmopia-stage-supabase-db-data,sanmopia-stage-supabase-db-config, andsanmopia-stage-supabase-storage-data. -
Supabase mounted SQL/YAML files must be world-readable, and
supabase/volumes/api/kong-entrypoint.shmust be world-readable and executable. Supabase Auth/PostgREST/Storage and Kong run as non-root users in the official images. -
PostgREST exposes
public,storagein stage.graphql_publicis not listed until GraphQL schema provisioning is added. -
Reservation-scoped family payment delegation is proved with three bounded commands. Run backend
pnpm supabase:smoke:stage:family-payment-delegationfor transactional RPC grant/replay/revoke/replay and PGMQtouch/deleteevidence, assemblypnpm authorization:smoke:family-payment-delegationfor SpiceDB permission convergence, and backendpnpm supabase:smoke:stage:family-payment-delegation-e2efor authenticated HTTP booking, queue dispatch, permission grant/removal, amount-ceiling enforcement, immutable payment actor capture, pre-registration replay, and registered reaper cleanup. Runpnpm authorization:bootstrapafter schema changes or stage recreation before this proof. These scripts are bounded and leave no browser process or one-off container. -
Run backend
pnpm supabase:smoke:stage:reservation-care-history-accessfor the authenticated family care-history authority matrix. It creates one real Restate booking, rejects a browser-supplied mother field, grants and replays booking-scoped read authority, converges SpiceDB, permits the delegated read, rejects cross-booking access, permits mother review readiness, rejects a forged family-delegate review/rating submission, revokes and replays the grant, removes SpiceDB permission, and rejects the post-revocation read. The shared booking-smoke wrapper now always invokes the registered test-data reaper after its downstream smoke, including failure paths; DB rows and reconstructed SpiceDB booking relations are read-back verified absent. -
Run backend
pnpm supabase:smoke:stage:reservation-to-settlement-completionfor the complete non-browser proof: booking, delegated family payment pre-registration, payment verification, service delivery, caregiver payout, branch settlement, headquarters settlement, and final settlement completion. The 2026-07-11 run passed for reservation92961f7e-7896-4441-af53-8fb8825d6fe2and actor snapshotda2c3402-0d85-52a2-b6b4-bcf7f6c189bf. -
Stage mother booking smoke context is created with
pnpm supabase:seed:mother-booking-context. The script creates or reuses a Supabase Auth mother user, upsertssanmopia_mother_booking_operational_contexts, upserts stage branch/member profile identity rows, touches the SpiceDB branch-operator tuple for the issued branch-operator Supabase Auth user id, registers the created Auth/profile/context rows underSANMOPIA_TEST_DATA_BATCH_KEY, and writes the mother session toSANMOPIA_STAGE_MOTHER_SESSION_PATH. It calls Traefik through the Mutagen forward and sets Host headers explicitly; do not depend on host DNS for*.sanmopia.localhost. Runpnpm authorization:bootstrapbefore this seed whenever the stage was recreated. The booking context’sbranchActor.roleuses the organization domain value such as지점 운영자; SpiceDB relation names stay technical values such asbranch_operator. -
Backend application code depends on the neutral workflow engine port in
application/platform/workflow_engine.py. Restate-specific naming stays in HTTP adapters and SDK service boundaries. -
Backend mounts the Restate SDK app at
/restate/v1; register it with Restate throughpnpm workflow:registerafterbackend-devandworkflow-state-devare running. The registration URL comes fromSANMOPIA_RESTATE_BACKEND_DEPLOYMENT_URLand must stay on the internal Compose network. The registered deployment exposesMemberPrivacyLifecycleWorkflow,ReservationBookingWorkflow,ReservationFinancialLifecycleWorkflow, andReservationPaymentWorkflow. The script runs insidedocs-devso Docker DNS names such asworkflow-state-devandbackend-devresolve. -
Reservation booking workflow starts through
POST /reservation-booking-workflow-starts. The payload may includeacceptedPriceCatalogQuoteand must carrycareEnvironmentso matching replay has the same pet/environment facts used at submit time. Booking then captures the accepted quote into immutable finalized charge rows and exposesfinalizedChargeSnapshotIdin workflow progress. -
Mother booking pages must receive booking operational context before submit: mother profile, branch context, branch actor, caregiver candidates/profiles, care environment, service options with price identifiers/amounts, and payment methods. Frontend must not synthesize branch ids, caregiver ids, service fees, payment methods, care environment, or actor ids. Missing context is a blocked submit state, not a fallback.
-
Financial lifecycle closeout starts through
POST /reservation-financial-lifecycle-workflow-starts, which sends the workflow to Restate. Progress reads useGET /reservation-financial-lifecycle-workflow-requests/{financialLifecycleWorkflowRequestId}. The Supabase financial lifecycle workflow ledger is service-role only; frontend clients use the backend API, not direct Data API table access. Direct financial lifecycle command routes remain for single-stage operator repair and tests. -
Browser-started reservation-to-settlement stage proof uses
backend-repo/scripts/stage_reservation_settlement_e2e.pyafter a booking is created and payment is completed. The driver seeds SpiceDB relationships with the existing stage branch/HQ actors, creates the service-balance closeout reference from Supabase, and calls the real financial lifecycle command routes in order: service delivery, caregiver payout, branch settlement, HQ settlement, and settlement completion. From the host, run it with Traefik hosts and stage env, for example:Terminal window JWT_SECRET=$(docker inspect sanmopia-stage-supabase-auth --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^GOTRUE_JWT_SECRET=//p')JWT_ISSUER=$(docker inspect sanmopia-stage-supabase-auth --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^GOTRUE_JWT_ISSUER=//p')SUPABASE_SERVICE_ROLE_KEY=$(docker inspect sanmopia-stage-backend --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^SUPABASE_SERVICE_ROLE_KEY=//p')SPICEDB_PRESHARED_KEY=$(docker inspect sanmopia-stage-backend --format '{{range .Config.Env}}{{println .}}{{end}}' | sed -n 's/^SPICEDB_PRESHARED_KEY=//p')JWT_SECRET="$JWT_SECRET" JWT_ISSUER="$JWT_ISSUER" \SUPABASE_SERVICE_ROLE_KEY="$SUPABASE_SERVICE_ROLE_KEY" \SPICEDB_PRESHARED_KEY="$SPICEDB_PRESHARED_KEY" \SUPABASE_DB_URL="postgresql://postgres:postgres@db:5432/postgres" \SUPABASE_URL="http://supabase.sanmopia.localhost:24055" \SPICEDB_HTTP_URL="http://authorization.sanmopia.localhost:24055" \SANMOPIA_STAGE_BACKEND_URL="http://api.sanmopia.localhost:24055" \PYTHONPATH=src:. uv run python scripts/stage_reservation_settlement_e2e.py <reservationBookingId>Browser orchestration must run through
pnpm playwright:reservation-settlement:bounded. The assembly runner creates a dedicated process group, enforcesSANMOPIA_PLAYWRIGHT_TIMEOUT_MS, handles interruption, and terminates surviving descendants before returning.This is a verification driver, not a product endpoint. Production ledger producers now issue caregiver payout, branch settlement, and HQ settlement lines from frozen backend facts. The remaining driver debt is fixture-backed quote/payment-product setup and browser-visible proof for every customer disclosure branch.
-
Positive service-balance disclosure proof uses the same driver with a due amount:
Terminal window JWT_SECRET="$JWT_SECRET" JWT_ISSUER="$JWT_ISSUER" \SUPABASE_SERVICE_ROLE_KEY="$SUPABASE_SERVICE_ROLE_KEY" \SPICEDB_PRESHARED_KEY="$SPICEDB_PRESHARED_KEY" \SUPABASE_DB_URL="postgresql://postgres:postgres@db:5432/postgres" \SUPABASE_URL="http://supabase.sanmopia.localhost:24055" \SPICEDB_HTTP_URL="http://authorization.sanmopia.localhost:24055" \SANMOPIA_STAGE_BACKEND_URL="http://api.sanmopia.localhost:24055" \PYTHONPATH=src:. uv run python scripts/stage_reservation_settlement_e2e.py \<reservationBookingId> --service-balance-amount-krw 25000Latest proof, 2026-07-09: booking
d137de9d-ebbc-4e90-8b7a-dd3e9d39836creachedsettledwith caregiver payout50000, branch settlement100000, HQ settlement50000,ledgerLineCount=3, customer instruction availabilityavailable, account-disclosure readinessready, emptyblockingFacts, andrawAccountFieldsVisible=false. Browser-created follow-up booking89696822-5ff7-470d-a7fc-8b6bca848ef4proved the same positive disclosure path from the Astro card flow. It reachedsettledwith mother-visible payable1681519, caregiver payout1581519, branch settlement100000, audited zero-amount HQ service-margin line,ledgerLineCount=3, instruction availabilityavailable, readinessready, emptyblockingFacts, andrawAccountFieldsVisible=false. -
2026-07-09 catalog-quote follow-up:
supabase:smoke:stage:reservation-bookingno longer buildsacceptedPriceCatalogQuotewith inline booking-script arithmetic. It usesstage_private_care_price_catalog_quote_payload(), which seeds and readsstage_care_price_catalog/stage-2026.07/stage-private-one-weekthrough Supabasesanmopia_price_catalog_import_batchesandsanmopia_price_catalog_entries, then quotes through the domainPriceCatalogQuotePolicy. DB proof on 2026-07-09 showed the batchimport_status=published, entry service-day count5, and entrypublished_atpresent. Bookinga97c8dfe-566f-4295-a266-3c1fa7ab0553accepted only reservation-contract-safe line componentscustomer_shareandheadquarters_platform_fee, then passed the positive service-balance settlement driver with caregiver payout50000, branch settlement100000, HQ settlement50000, disclosure readinessready, and final lifecyclesettled. The wrapper proof6f95ad57-d702-40fb-bfdb-6637a801c42ealso passedsupabase:smoke:stage:reservation-to-settlement-completion. 2026-07-10 positive-balance wrapper proof addedsupabase:smoke:stage:reservation-to-settlement-positive-balance; bookinge857c9ac-ef1a-4011-80ca-be50bd4de6acreachedsettledwith service balance25000, caregiver payout50000, branch settlement100000, HQ settlement50000,ledgerLineCount=3, and account-disclosure readinessready. The smoke registered test-data reaper batchstage-reservation-booking:e857c9ac-ef1a-4011-80ca-be50bd4de6acwith25references, including branch settlement rows bybranch_settlement_id. 2026-07-10 settled-refund-impact proof addedsupabase:smoke:stage:reservation-to-settlement-refund-impact; booking9700b2e4-0b25-4978-9477-2c864c3f29c1reachedsettled, then provider refund eventstage-provider-refund-result-20260709201933-65135721verified paid-row mutation, settlement reversal, and six reporting refresh requests. -
2026-07-09 payment-product catalog follow-up:
pnpm supabase:migrateapplied20260709145000_payment_product_plan_price_catalog_references.sqlafter replay-safe cleanup for older partially-applied stage migrations. DB proof showspayment_product_plan_catalog.v5has six rows, and exactly two rows carry price-catalog references:reservation.customer_share.depositmaps tocustomer_share_deposit, andreservation.customer_share.balancemaps tocustomer_share_balance; both referencecare_price_catalog/accepted_reservation_quote/customer_share. -
Payment UI entry first calls
POST /reservation-payment-pre-registrations. The body contains onlypaymentId; bearer identity andIdempotency-Keyare required. Booking already persisted the waiting obligation. The backend loads reservation, method, purpose, product plan, and amount, checks Supabase member identity plus SpiceDB capability and any family delegation ceiling, then freezes an immutable actor/authority snapshot before Kill Bill UI opens. The backend app factory must pass the configuredclockintoReservationPaymentApi; otherwise stage returns503withreservation payment clock is not configured. 2026-07-08 stage browser prooffrontend-repo/output/playwright/stage-payment-pre-reg-20260708163501-booking-desktop-payment-pre-registered.pngcovers booking creation, progress polling, charge summary, and201 pre_registeredfrom the payment pre-registration endpoint. -
Kill Bill billing client operations start through
POST /reservation-payment-workflow-startsand progress reads throughGET /reservation-payment-workflow-requests/{paymentWorkflowRequestId}. The Supabase payment workflow ledger is service-role only; frontend clients use the backend API, not direct Data API table access. 2026-07-08 stage proofRPW-STAGE-20260708163933completed after the DB repair migration restoredsanmopia_caregiver_payout_acknowledgementsand shortened the pgmq queuesanmopia_booking_term_invalidation_execunder the 48-character pgmq limit. The workflow now verifies debug payment and stops at the expected business boundary: settlement continuation waits for service-delivery completion facts. -
2026-07-08 follow-up scenario continued the same reservation
90ac43cf-146c-44de-adf6-ebce7e4042cdfrom service delivery through caregiver payout, Branch settlement, HQ settlement, andsettled. Runtime blockers fixed during the proof: service-delivery command replay is now idempotent when the same completion fact is already recorded, and the customer service-balance instruction projection now uses the shorter canonical Supabase tablesanmopia_customer_service_balance_instruction_projectionsbecause the previous long physical table name exceeded PostgreSQL identifier length and was truncated. Proof workflowsettlement-continuation:RPW-STAGE-20260708163933:service-replay-20260708170535completed withfinancialStage = service_delivered; direct repair commands then advanced caregiver payout950000, Branch settlement1681519, HQ settlement90000, and finalsettledstate. -
Kill Bill push callbacks post a generic
ExtBusEventtoPOST /killbill-payment-events. Payment callbacks useobjectType=PAYMENT,objectIdas the payment id,tenantIdas tenant evidence, andeventType=PAYMENT_SUCCESSorPAYMENT_FAILED. The JSON encodedmetaDatasuppliespaymentTransactionId,transactionType,status, amount, currency, andeffectiveDate.PURCHASEorCAPTUREsuccess starts paid verification;REFUNDsuccess records refund completion;VOIDsuccess records cancellation. Failure handling also derives fromtransactionType. Invented provider events such asPAYMENT_REFUNDEDorPAYMENT_CANCELLEDare invalid. -
Kill Bill push notifications do not include a webhook signature.
KILLBILL_WEBHOOK_SECRETis therefore an internal callback-admission token, not provider signature evidence. A trusted internal Traefik route must restrict the callback source and inject the token; no public router may inject it. Until that deployment boundary is configured, real Kill Bill callback E2E remains incomplete. First acceptance atomically persists the immutable inbox row and sendssanmopia_payment_webhook_processingthrough one Supabase RPC. Canonical payload replay returnsduplicatewithout requeue. Same event identity with a different payload digest returnsquarantined, appends repair evidence, and applies no business effect. Queue workers startReservationPaymentWorkflowonly for paid verification operations. 2026-07-12 refund callback proofsupabase:smoke:stage:killbill-refund-callback-to-settlement-reversalposted an official-shape Kill BillREFUNDcallback through the hosted route, proved duplicate replay idempotency, dispatchedsanmopia_payment_webhook_processing, recorded the provider refund result from the immutable refund ledger, advanced the paid row topartial_refunded, persisted settlement reversal, requested all six reporting refresh targets, and reaped the registered stage batch. -
Payment-change secure handoff foundation is applied through migrations
20260711002000_payment_change_handoff_authorization.sqland20260711002100_payment_change_handoff_authorization_index_name.sql. Stage transaction proof usedservice_role: first command advanced adjustment and payment revisions from0to1, created one prepared handoff and one audit; exact replay returnedreplayed=truewithout a second audit. Directauthenticatedexecution failed withpermission denied for function sanmopia_authorize_payment_change_handoff. Proof rolled back and left zero adjustment, payment, and audit rows. Runtime remains incomplete until backend interface obtains a payer/booking-bound SpiceDB decision and invokes this RPC; browser authorization evidence is forbidden. -
authorization-migraterunsspicedb migrate headagainst Supabase Postgres beforeauthorization-devserves traffic.authorization-schema-bootstrapwritesSANMOPIA_SPICEDB_SCHEMA_PATHto SpiceDB through Zed after Mutagen has synced backend source. The schema file lives in backend source and remains the single schema source of truth. -
Backend authorization checks use
SANMOPIA_SPICEDB_HTTP_URL=http://authorization-dev:8443on the internal Compose network. -
SpiceDB HTTP is also routed through Traefik with
SANMOPIA_SPICEDB_HTTP_HOST=authorization.sanmopia.localhostso stage smoke scripts can seed relationships without exposing a dedicated host port. Requests still requireSANMOPIA_SPICEDB_PRESHARED_KEY.
Frontend runtime:
- Frontend package scripts must not hardcode
--host,--port,0.0.0.0, orlocalhost. - Frontend interactive state must use Nanostores. Page scripts may bind DOM
nodes, but submit/progress/payment interaction state belongs in Nanostores
under
src/ui/**orsrc/lib/**, not scattered local mutable variables. src/features/**is deprecated migration debt, not an allowed target architecture. New frontend code must use Astro pages plus atomic UI (src/ui/atoms,src/ui/molecules,src/ui/organisms,src/ui/templates) and typed API clients. Business/security decisions such as RBAC, reservation stage transitions, pricing, payment readiness, caregiver payout, branch settlement, HQ settlement, matching, document access, and audit decisions stay in backend/contracts and are rendered by frontend.- Frontend
8a9a6f5makesreservation-smoke-scenariosPlaywright-only. No smoke settlement DTO, endpoint literal, ledger preparation, proof state, or rendering remains under productionsrc/**; ESLint blocks reintroduction. Playwright may invoke the dev-smoke endpoint after browser booking, then opens the real branch settlement projection for visual proof. - Astro dev host comes from
ASTRO_DEV_HOSTorASTRO_HOST. - Astro dev port comes from
ASTRO_DEV_PORTorASTRO_PORT. - Astro preview host comes from
ASTRO_PREVIEW_HOSTorASTRO_HOST. - Astro preview port comes from
ASTRO_PREVIEW_PORTorASTRO_PORT. - Compose routing still uses
SANMOPIA_FRONTEND_CONTAINER_PORTand Traefik labels.
Validation:
pnpm stage:checkpnpm mutagen:renderdocker compose --env-file .env.example -f docker-compose.yml pull assembly-staticpnpm mutagen:startpnpm supabase:migratepnpm authorization:bootstrappnpm workflow:registerpnpm test-data:reappnpm supabase:seed:mother-booking-contextpnpm smoke:mother-booking-workflowpnpm preview:e2e:smoke( cd ../backend-repo && pnpm run supabase:smoke:stage:reservation-booking && pnpm run supabase:smoke:stage:payment-continuation-after-service-delivery && pnpm run supabase:smoke:stage:financial-lifecycle-closeout-guard && pnpm run supabase:smoke:stage:service-balance-closeout)SANMOPIA_MOTHER_BOOKING_ACCESS_TOKEN="$(node -e 'console.log(JSON.parse(require("fs").readFileSync("/tmp/sanmopia-stage-mother-session.json","utf8")).access_token)')" \ docker compose --env-file .env.example -f docker-compose.yml up -d --force-recreate frontend-devdocker compose --env-file .env.example -f docker-compose.yml configstat -c '%g' /var/run/docker.sockBuild and routing rules:
- Image builds run only through
.github/workflows/assembly-image-release.ymlas a thin call to the organization-ownedcompany-remote-build-v4contract. Sanmopia owns the source,ghcr.io/yamonco/sanmopia-modernization-assemblyimage namespace, digest, and release lifecycle. The FractalOps executor only supplies the self-hosted runner, shared Kubernetesfractalops-buildxbuilder, registry cache, scan, signing, and attestation. - Compose and Mutagen never build images.
SANMOPIA_ASSEMBLY_IMAGEis a GHCR digest produced by the release workflow; stage startup only pulls and runs it. docker build,docker buildx build, Composebuild, and Buildx--loadare forbidden on developer hosts. Image compilation uses the FractalOpsbuildx-cluster-buildphase only.- Runtime pulls are not image builds. Reuse the existing self-hosted Supabase
stage; do not run
supabase startorsupabase stop, which would create a second CLI-managed runtime and bypass the project stage lifecycle. - Compose services use Traefik labels plus container
expose; applicationportsare not allowed unless a manual says why host exposure is required. - Traefik labels must declare router host, service port, middleware ownership, and network. Inline hostnames or ports in app code are drift.
- Mutagen config is generated, not hand-edited. Lifecycle is render, start,
flush when needed, terminate, then drift check with
docker compose config. - BuildKit workflow failures, Mutagen drift, and Traefik label mismatch are stage failures, not frontend/backend feature bugs.
Mother booking context smoke:
token="$(node -e 'console.log(JSON.parse(require("fs").readFileSync("/tmp/sanmopia-stage-mother-session.json","utf8")).access_token)')"curl -H 'Host: api.sanmopia.localhost' \ -H "Authorization: Bearer $token" \ http://127.0.0.1:24055/mother-booking-operational-contextMother booking workflow smoke:
pnpm authorization:bootstrappnpm workflow:registerpnpm test-data:reappnpm supabase:seed:mother-booking-contextpnpm smoke:mother-booking-workflowPreview E2E smoke:
pnpm preview:e2e:smokeThis command refreshes the stage Supabase Auth session before browser or API
smoke work. Use it when /mother-booking-operational-context returns
bad_jwt; do not reuse an old /tmp/sanmopia-stage-mother-session.json as
proof.
pnpm supabase:seed:mother-booking-context now writes both
/tmp/sanmopia-stage-mother-session.json and
/tmp/sanmopia-stage-branch-operator-session.json; browser pricing-settlement
preview should use the branch-operator session instead of bypassing auth or
embedding a fixture projection.
Latest local proof, 2026-07-07: after pnpm mutagen:flush and recreating
backend-dev, pnpm smoke:mother-booking-workflow completed with
reservationBookingRequestId=stage-booking-1783421202551,
workflowStatus=completed, and
reservationId=76e2a393-4399-4554-b1c6-870446d26be1.
Follow-up proof, 2026-07-09: pnpm preview:e2e:smoke refreshed the Supabase
Auth session, removed the previous bad_jwt blocker, and completed with
reservationBookingRequestId=stage-booking-1783545176653,
workflowStatus=completed, reservationId=f4a41d1d-96b2-4923-8b56-a7058c0b3d09,
and finalizedChargeSnapshotId=f4a41d1d-96b2-4923-8b56-a7058c0b3d09. The same
run passed backend stage settlement smoke:
supabase:smoke:stage:reservation-booking,
supabase:smoke:stage:payment-continuation-after-service-delivery,
supabase:smoke:stage:financial-lifecycle-closeout-guard, and
supabase:smoke:stage:service-balance-closeout. Frontend Playwright submit
created reservation 8182ac94-1884-45da-9e68-d16b280abdcb, returned
progressStatus=확정, paymentWorkflowStatus=completed · 결제 진행 완료, and
mother-visible payable 1,681,519원 with no failed browser requests.
Follow-up proof, 2026-07-09: pnpm preview:e2e:smoke passed again with
reservationBookingRequestId=stage-booking-1783545570372,
workflowStatus=completed, reservationId=9ee119e4-4964-4569-a13b-1da10f649cf6,
and finalizedChargeSnapshotId=9ee119e4-4964-4569-a13b-1da10f649cf6. The same
seed run emitted branchOperatorSessionPath=/tmp/sanmopia-stage-branch-operator-session.json
for authenticated /pricing-settlement browser verification.
Follow-up proof, 2026-07-09: frontend pnpm playwright:operator-projection
with OPERATOR_PROJECTION_BRANCH_PROFILE_ID=00000000-0000-0000-0000-000000000101
and /tmp/sanmopia-stage-branch-operator-session.json reached
settlementBoardState=ready, rendered 백엔드 projection 조회 완료, injected
operator auth cookies, and reported no response errors. At that point the
browser-visible blocker was settlementDataRoomReadiness=contract_gap; the next
proof below closes the readiness contract gap while leaving deeper
data-room detail/action grants for the settlement-room slice.
Follow-up proof, 2026-07-09: after the settlement data-room readiness contract
landed and sanmopia-stage-backend restarted, direct
GET /branch-settlement-reconciliation-exports/00000000-0000-0000-0000-000000000101?exportFormat=json
returned settlementDataRoomReadiness.collection=branch_to_headquarters_settlement,
readinessStatus=empty, null settlement period coverage, and revision/evidence
keys from the frozen export dataset. pnpm playwright:operator-projection then
passed on desktop and mobile with settlementBoardState=ready,
settlementDataRoomReadiness=branch_to_headquarters_settlement, zero console
events, zero failed requests, and zero HTTP response errors.
Follow-up proof, 2026-07-09: the tracked frontend runner
RESERVATION_FLOW_SUBMIT=1 pnpm playwright:reservation-card-flow submitted the
Astro card flow through the real stage API with request
RBQ-e31f1bf8-3b04-4be4-846c-ee22de68b9e3, booking
c1c75eca-9f6a-4072-87f2-ea8b054a5ce9, progressStatus=확정,
paymentWorkflowStatus=completed · 결제 진행 완료, and mother-visible payable
1,681,519원. Browser evidence includes 202 from
POST /reservation-booking-workflow-starts, 200 from the workflow progress
read, and 200 from the mother-visible charge summary. Desktop dev-mode
artifacts still show Vite net::ERR_ABORTED reload noise after success, but no
console errors or HTTP response errors. The same frontend lane now has tracked
session refresh helpers in scripts/playwright/supabase_session.mjs; external
fixture generation remains necessary only when both Supabase access and refresh
tokens are expired or revoked.
Follow-up proof, 2026-07-09: after refreshing stage Supabase sessions through
SANMOPIA_STAGE_SUPABASE_HTTP_ORIGIN=http://172.19.0.5:8000 pnpm supabase:seed:mother-booking-context,
the container frontend route http://172.19.0.3:4321/mother/booking completed
desktop card-flow submit with request
RBQ-46bb19f9-af71-4e18-8dda-55c2454e3012, booking
31b0dcce-e941-402b-bc53-4610cdb9ac9e, progressStatus=확정,
paymentWorkflowStatus=completed · 결제 진행 완료, and mother-visible payable
1,681,519원. Backend continuation on that same UI-created booking passed
supabase_stage_payment_continuation_after_service_delivery_smoke.py and
supabase_stage_existing_reservation_settlement_completion_smoke.py, producing
service_balance_closeout_event_id=f21c7f0c-0cf8-4164-8ba1-5e56499ae437,
branch_settlement_dashboard_rows=5, and
branch_settlement_board_line_items=1.
If app.sanmopia.localhost:24055 is owned by a host Astro node process instead
of the stage frontend container or Traefik, verify its
SANMOPIA_BACKEND_INTERNAL_URL before using it as proof. A stale host process
pointing at a removed backend IP can show 예약 운영 컨텍스트 확인 필요: fetch failed
even while the containerized stage stack is healthy.
Follow-up proof, 2026-07-07: after pnpm stage:check and
pnpm supabase:seed:mother-booking-context, pnpm smoke:mother-booking-workflow
completed with reservationBookingRequestId=stage-booking-1783421745320,
workflowStatus=completed, reservationId=2ca927ad-fb34-4cfc-820d-aa22ab16e064,
and finalizedChargeSnapshotId=2ca927ad-fb34-4cfc-820d-aa22ab16e064.
Follow-up proof, 2026-07-11: browser request
RBQ-78c9307d-0534-4d87-9f0e-90641786b683 created booking
d0d51303-703e-40c0-8bcc-95dcc9e6ef88, reached progressStatus=확정, completed
the payment workflow, and rendered backend payable 1,681,519원. Backend
settlement reached financial_stage=settled with branch settlement
63aeb220-f2ac-5d49-b49f-86770a516980. The runner then exposed a test-data
reaper FK ordering bug; backend f63af0b15f14e398c6c879bea595316b8afab0b0
creates the batch before references and proves that order with a colocated test.
Backend afd5fc7383d5cf4f907501dba189a42c52cd59cb then replaced obsolete Restate
ingress routing with official asynchronous /{workflow}/{key}/{handler}/send;
API smoke completed in 2.35 seconds with workflow status completed.
Fresh bounded browser proof submitted
RBQ-fe3512da-50b4-43ac-a2c8-64d86a9d9fbf, created booking
1b291ba5-17a2-4756-9767-1819846bdd6d, reached progressStatus=확정, and kept
mother-visible payable 1,681,519원 through final financialStage=settled.
Settlement produced caregiver payout 1,581,519원, branch settlement
100,000원, HQ settlement 0원, and three ledger lines. Account disclosure was
not_required with no raw account fields. Authenticated branch projection was
ready with data-room readiness branch_to_headquarters_settlement; console,
failed-request, and HTTP-error counts were zero. Post-run process verification
found zero Playwright/Chromium processes and 15 GiB available host memory.
Bounded backend stage proof, 2026-07-11: backend b0bbd21 made failed
reservation-booking reaping resumable and catalog-ordered. The guarded retry
reaped three expired batches and 90 referenced rows; the next dry run reported
expiredBatchCount=0. Then
pnpm supabase:smoke:stage:reservation-to-settlement-completion created request
RBQ-STAGE-20260711150353810171, booking
3ce972db-996b-4c8b-a3f2-b21fc04a9b5a, and payment
3ce972db996b4c8ba3f2b21fc04a9b5a; exercised family payment delegation,
continued after service delivery, and completed settlement with closeout event
63d333ae-877f-48ac-87d5-6c2091f44d84. The branch dashboard returned 55 rows
and the booking board returned one line item. This is real stage API/database
proof without a browser and does not claim the family-sponsored contract
acceptance matrix.
Current authenticated backend proof, 2026-07-11: backend 48abf1c aligned the
shared booking fixture with the actor-free public contract, separated booking
request correlation from booking UUID correlation, normalized exact Restate
replay, and made registered cleanup resumable through scalar Supabase RPCs.
Family sponsorship created completed booking
1ad6710c-1108-49bc-84b7-9b2a9cae3f33 with grant
aa2fc2ce-962f-4b07-a9ed-e9179985bcb0. A separate full chain created booking
c98d54ab-c848-413d-a628-5f7eea1c5f2c, continued payment and service delivery,
completed settlement with closeout event
bda55524-73ea-40b9-bd2e-09dc11f7695b, returned one branch settlement line,
and automatically reaped the registered booking batch. Backend ddd1559 then
proved the combined path on one family-sponsored booking
faa26ffd-0a3b-4e51-b87f-132b2d27d035, freezing payer snapshot
c2dafb28-f3bd-50a2-a901-eb0a9ac14280 and completing closeout event
e59c748a-f4a8-4262-a709-a1e88b3a54a7. Final cleanup verified both DB reaping
and removal of SpiceDB booking view permission for mother, branch operator,
and caregiver. Stage branch seed data carries explicit source id 101; profile
UUID remains authorization identity.
Family-authority invalidation proof, 2026-07-12: backend 5414fa4 extends
pnpm supabase:smoke:stage:family-sponsored-reservation-e2e with two
backend-owned negatives. An accepted draft bound to a revoked and replaced
sponsorship grant is rejected, and an accepted draft bound to the previous
service-contract definition version is rejected after an optimistic catalog
rollover. A valid draft under the replacement grant and current contract
version still completed booking 21fb9787-416e-49d6-ab0f-ca5e8e3d5311.
pnpm supabase:smoke:stage:family-sponsored-reservation-to-settlement-e2e
repeated the matrix on booking 3130d737-d45f-41ef-a4c6-d85cdcd42783, then
completed delegated payment, service delivery, closeout
bd63b47d-f2a2-49dd-9a79-e5ae6809fdc7, one settlement line, SpiceDB denial,
and registered DB reaping. The run uses no browser and restores or reaps every
temporary contract and family fixture.
Reservation care-history authority proof, 2026-07-12: backend 903e68d and
contract e4cc9ed expose the runtime and public API. Backend
pnpm supabase:smoke:stage:reservation-care-history-access completed booking
81af6692-7703-4da4-89cb-9300bfb5b769 and grant
b7d413de-894e-4ada-894a-457823e79815. The authenticated HTTP matrix proved
grant/revoke replay, booking scope, delegated read-only access, forged mother
review/rating denial, and SpiceDB convergence/removal. The wrapper then reaped
the booking batch and verified mother, branch-operator, and caregiver booking
view relationships absent. No browser process was started.
Family authority directory stage proof, 2026-07-13: backend
pnpm supabase:smoke:stage:reservation-care-history-access completed booking
f175bd00-441f-4801-b21d-3081bfa75682 and grant
aecf7aa6-d034-407d-9148-74deb5f87d1c. The smoke now also proves active
delegate own authority-directory visibility, no authorityEvidence leak,
non-admin administration denial, revocation, post-revoke disappearance from
the delegate’s directory, SpiceDB cleanup, and registered test-data reap. During
this run the first attempt returned 404 for authority-directory because the
backend container still had a stale imported app after source sync. The proof
path was pnpm mutagen:flush, restart only backend-dev, then rerun the smoke.
Kill Bill callback stage proof, 2026-07-12: backend
pnpm supabase:smoke:stage:killbill-payment-event-callback completed booking
06bb3c50-aca5-4ab4-97e2-fd8a955729a6, pre-registered payment
06bb3c50aca54ab497e2fd8a955729a6, posted an official-shape
PAYMENT_SUCCESS ExtBus callback for payment
06bb3c50aca54ab497e2fd8a955729a6, persisted inbox event
killbill-event:sha256:3af8a2681cbb1d12985bf4eda20e0ede5e162efa50e9930b5e9d66d52165d08f,
queued one sanmopia_payment_webhook_processing message 7, proved duplicate
replay did not requeue, dispatched the payment-webhook worker, verified
completed workflow
killbill:PAYMENT_SUCCESS:06bb3c50aca54ab497e2fd8a955729a6:stage-killbill-tx-20260712131444-9f9a82b5,
verified payment status approved, and reaped the booking batch.
No Playwright or docs dev server was started.
Callback-to-settlement proof, 2026-07-12: backend
pnpm supabase:smoke:stage:killbill-callback-to-settlement-completion
completed booking a8a05449-5f1b-466b-8b02-73286d09e406, approved payment
a8a054495f1b466b8b0273286d09e406 through Kill Bill callback event
killbill-event:sha256:a72c2173794b7087cff217a68228391cb0f2549c88d5a07a79ed4fdf8382cfb4,
reused payment workflow
killbill:PAYMENT_SUCCESS:a8a054495f1b466b8b0273286d09e406:stage-killbill-tx-20260712131915-718b0da1,
advanced service delivery, caregiver payout, branch settlement, headquarters
settlement, and final settled; branch settlement dashboard returned rows and
the settlement board exposed line items. No Playwright or docs dev server was
started.
Follow-up authorization proof, 2026-07-11: pnpm authorization:bootstrap
applied backend 871defbfb13617a3b39bb4a32ce31b64d6cb466d. Live SpiceDB checks allowed
mother payment, authorized-payer receipt, branch refund, and HQ payment. The
assigned caregiver retained booking view but payment, refund, and receipt all
returned denied. This proves the FAM-004 graph boundary, not yet persisted payer
lineage or complete authenticated HTTP coverage.
Playwright resource gate:
- one browser process and sequential viewports only;
- wall-clock timeout plus process-group
SIGTERM/SIGKILLcleanup; - always close context/browser, including Playwright MCP before agent shutdown;
- verify zero runner, Playwright daemon, headless Chromium, and temporary Playwright profile processes after every run;
- do not call a run successful until both business assertions and process cleanup pass.
Migration runner drift guard:
pnpm supabase:migraterecords applied SQL filenames inprivate.sanmopia_stage_applied_migrations.- If an older stage DB already has the reservation collaboration extra-service
wrapper functions and child table, the runner records
20260702195000_apply_extra_service_days_collaboration.sqlas applied before replay. This prevents a falsefunction already existsstop while preserving the canonical migration order for new stages. - The same drift guard records
20260702201000_reservation_caregiver_assignment_collaboration.sqlwhen the caregiver assignment table and wrapper-chain functions already exist.
Stage recovery:
mutagen project -f .mutagen.project.generated.yml terminatedocker compose --env-file .env.example -f docker-compose.yml downdocker volume rm sanmopia-stage-supabase-db-data sanmopia-stage-supabase-db-config sanmopia-stage-supabase-storage-datapnpm mutagen:renderdocker compose --env-file .env.example -f docker-compose.yml pull assembly-staticpnpm mutagen:startpnpm supabase:migratepnpm authorization:bootstrappnpm workflow:registerpnpm test-data:reappnpm supabase:seed:mother-booking-contextpnpm smoke:mother-booking-workflow