Skip to content

Development Stage

Development runs through assembly-repo/docker-compose.yml.

Rules:

  • .env.example is the committed stage source of truth.

  • SANMOPIA_STAGE_ENV_FILE selects another env file.

  • manifest.yaml pins the exact assembly, contract, backend, and frontend Git revisions. Run pnpm repositories:check before 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=false must 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_TOKEN and 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:migrate after db is healthy. The assembly command delegates to the backend’s pinned Supabase CLI and uses supabase db push; official supabase_migrations.schema_migrations history is the only ledger. If an existing stage has Sanmopia tables but empty official history, the runner performs a one-time supabase migration repair baseline before pushing. Never restore a custom psql file loop or application migration ledger.

  • Migration discovery reads backend-repo/supabase/migrations from the host, independent of Mutagen. Mutagen is still required for live backend source: after application code changes, run pnpm mutagen:flush and restart only backend-dev before HTTP proof. Use pnpm --dir ../backend-repo supabase:db:push:stage -- --dry-run to verify that stage and repository migration histories converge without repairing history or applying migrations. The env equivalent is SANMOPIA_SUPABASE_DB_PUSH_DRY_RUN=true pnpm supabase:migrate.

  • Stage smoke and scenario fixtures must register themselves in sanmopia_test_data_batches plus sanmopia_test_data_references. SANMOPIA_TEST_DATA_DEFAULT_TTL_HOURS sets the default expiry. pnpm test-data:reap is a dry-run that fails when expired fixture batches remain in a reachable stage. Actual deletion requires SANMOPIA_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 an EXIT trap. 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.example must be signed by SANMOPIA_SUPABASE_JWT_SECRET. pnpm stage:check verifies both the anon and service_role role claims because Kong/PostgREST reject mismatched keys only at runtime.

  • Runtime smoke for privacy automation uses pnpm supabase:cron:smoke; stale withdrawn-token smoke uses pnpm privacy:stale-token:smoke with SANMOPIA_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_required facts, paid payment closeout must use a finalized charge snapshot that acknowledges the latest impact plan, policy fingerprint, and impact fact ids. When those changes create caregiver_assignment_review_required facts, the assignment optimizer must acknowledge the latest impact plan before running the solver for that booking. When they create daily_report_selection_required facts, 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=unconfined in 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-compose is not the standard because upstream marks it deprecated in Mutagen v0.18.

  • The backend process executes /workspace/backend-repo from the sanmopia-stage-backend-source named volume. If pnpm mutagen:list shows 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 only sanmopia-stage-backend.

  • docs-dev is an explicit docs Compose profile and is not part of normal Mutagen stage startup. Start it only for human review with docker compose --env-file .env.example --profile docs up -d --no-deps docs-dev and stop it after review. The service waits for both assembly and contract source sync, mounts contract source read-only, and starts pnpm docs:dev without Astro --force dependency 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, and SANMOPIA_DOCS_NODE_MAX_OLD_SPACE_MB bound host impact during long reviews.

  • frontend-dev waits for frontend source, installs through pnpm, and runs Astro with SANMOPIA_ASTRO_DEV_HOST plus its service-specific container port.

  • frontend-dev mounts contract_source at /workspace/contract-repo:ro so 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/booking must 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-dev receives browser-facing PUBLIC_SANMOPIA_API_BASE_URL and server-only SANMOPIA_BACKEND_INTERNAL_URL. SSR API calls must prefer the internal URL because *.sanmopia.localhost hostnames are routed by Traefik/Mutagen for browsers, not by Docker DNS inside the frontend container.

  • SANMOPIA_MOTHER_BOOKING_ACCESS_TOKEN is a dev-only SSR smoke input. Populate it from SANMOPIA_STAGE_MOTHER_SESSION_PATH only when recreating frontend-dev for 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, and KILLBILL_API_URL. The API URL must include the /1.0/kb base path. Stage uses BILLING_CLIENT_MODE=debug so 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 under BILLING_CLIENT_MODE=killbill. Production must use BILLING_CLIENT_MODE=killbill with real Kill Bill secrets.

  • backend-dev runs FastAPI through uv run uvicorn sanmopia_modernization.main:app; Traefik routes to the internal container port and Compose still publishes no host ports.

  • backend-dev waits for pyproject.toml before calling uv run because source arrives through Mutagen into a named volume. Do not replace this with host bind mounts.

  • backend-dev mounts contract_source read-only at /workspace/contract-repo. The backend’s uv path dependency imports the generated sanmopia-python-contracts package from that repository; do not copy generated transport models into backend source.

  • backend-dev gets PYTHONPATH from SANMOPIA_BACKEND_PYTHONPATH so the src-layout backend imports consistently under Uvicorn and tests.

  • workflow-state-dev is the Restate stage service. It exposes ingress 8080, admin/UI 9070, and fabric 5122 only on Compose networks; backend commands use RESTATE_INGRESS_URL=http://workflow-state-dev:8080.

  • db, auth, rest, realtime, storage, imgproxy, and kong are the self-hosted Supabase core in stage. Backend uses http://kong:8000 and postgresql://postgres:...@db:5432/postgres on the internal network. Frontend uses SANMOPIA_SUPABASE_PUBLIC_URL through Traefik/Mutagen. Compose still publishes no Supabase host port.

  • Supabase Postgres binds POSTGRES_USER from SANMOPIA_POSTGRES_ADMIN_USER, which pnpm stage:check constrains to supabase_admin. Do not set it to postgres: the image migrate.sh connects as supabase_admin, runs init scripts, creates/demotes the postgres role, and then applies migrations. If a bad stage boot already initialized the volume without supabase_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, and sanmopia-stage-supabase-storage-data.

  • Supabase mounted SQL/YAML files must be world-readable, and supabase/volumes/api/kong-entrypoint.sh must be world-readable and executable. Supabase Auth/PostgREST/Storage and Kong run as non-root users in the official images.

  • PostgREST exposes public,storage in stage. graphql_public is 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-delegation for transactional RPC grant/replay/revoke/replay and PGMQ touch/delete evidence, assembly pnpm authorization:smoke:family-payment-delegation for SpiceDB permission convergence, and backend pnpm supabase:smoke:stage:family-payment-delegation-e2e for authenticated HTTP booking, queue dispatch, permission grant/removal, amount-ceiling enforcement, immutable payment actor capture, pre-registration replay, and registered reaper cleanup. Run pnpm authorization:bootstrap after 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-access for 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-completion for 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 reservation 92961f7e-7896-4441-af53-8fb8825d6fe2 and actor snapshot da2c3402-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, upserts sanmopia_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 under SANMOPIA_TEST_DATA_BATCH_KEY, and writes the mother session to SANMOPIA_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. Run pnpm authorization:bootstrap before this seed whenever the stage was recreated. The booking context’s branchActor.role uses the organization domain value such as 지점 운영자; SpiceDB relation names stay technical values such as branch_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 through pnpm workflow:register after backend-dev and workflow-state-dev are running. The registration URL comes from SANMOPIA_RESTATE_BACKEND_DEPLOYMENT_URL and must stay on the internal Compose network. The registered deployment exposes MemberPrivacyLifecycleWorkflow, ReservationBookingWorkflow, ReservationFinancialLifecycleWorkflow, and ReservationPaymentWorkflow. The script runs inside docs-dev so Docker DNS names such as workflow-state-dev and backend-dev resolve.

  • Reservation booking workflow starts through POST /reservation-booking-workflow-starts. The payload may include acceptedPriceCatalogQuote and must carry careEnvironment so matching replay has the same pet/environment facts used at submit time. Booking then captures the accepted quote into immutable finalized charge rows and exposes finalizedChargeSnapshotId in 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 use GET /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.py after 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, enforces SANMOPIA_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 25000

    Latest proof, 2026-07-09: booking d137de9d-ebbc-4e90-8b7a-dd3e9d39836c reached settled with caregiver payout 50000, branch settlement 100000, HQ settlement 50000, ledgerLineCount=3, customer instruction availability available, account-disclosure readiness ready, empty blockingFacts, and rawAccountFieldsVisible=false. Browser-created follow-up booking 89696822-5ff7-470d-a7fc-8b6bca848ef4 proved the same positive disclosure path from the Astro card flow. It reached settled with mother-visible payable 1681519, caregiver payout 1581519, branch settlement 100000, audited zero-amount HQ service-margin line, ledgerLineCount=3, instruction availability available, readiness ready, empty blockingFacts, and rawAccountFieldsVisible=false.

  • 2026-07-09 catalog-quote follow-up: supabase:smoke:stage:reservation-booking no longer builds acceptedPriceCatalogQuote with inline booking-script arithmetic. It uses stage_private_care_price_catalog_quote_payload(), which seeds and reads stage_care_price_catalog / stage-2026.07 / stage-private-one-week through Supabase sanmopia_price_catalog_import_batches and sanmopia_price_catalog_entries, then quotes through the domain PriceCatalogQuotePolicy. DB proof on 2026-07-09 showed the batch import_status=published, entry service-day count 5, and entry published_at present. Booking a97c8dfe-566f-4295-a266-3c1fa7ab0553 accepted only reservation-contract-safe line components customer_share and headquarters_platform_fee, then passed the positive service-balance settlement driver with caregiver payout 50000, branch settlement 100000, HQ settlement 50000, disclosure readiness ready, and final lifecycle settled. The wrapper proof 6f95ad57-d702-40fb-bfdb-6637a801c42e also passed supabase:smoke:stage:reservation-to-settlement-completion. 2026-07-10 positive-balance wrapper proof added supabase:smoke:stage:reservation-to-settlement-positive-balance; booking e857c9ac-ef1a-4011-80ca-be50bd4de6ac reached settled with service balance 25000, caregiver payout 50000, branch settlement 100000, HQ settlement 50000, ledgerLineCount=3, and account-disclosure readiness ready. The smoke registered test-data reaper batch stage-reservation-booking:e857c9ac-ef1a-4011-80ca-be50bd4de6ac with 25 references, including branch settlement rows by branch_settlement_id. 2026-07-10 settled-refund-impact proof added supabase:smoke:stage:reservation-to-settlement-refund-impact; booking 9700b2e4-0b25-4978-9477-2c864c3f29c1 reached settled, then provider refund event stage-provider-refund-result-20260709201933-65135721 verified paid-row mutation, settlement reversal, and six reporting refresh requests.

  • 2026-07-09 payment-product catalog follow-up: pnpm supabase:migrate applied 20260709145000_payment_product_plan_price_catalog_references.sql after replay-safe cleanup for older partially-applied stage migrations. DB proof shows payment_product_plan_catalog.v5 has six rows, and exactly two rows carry price-catalog references: reservation.customer_share.deposit maps to customer_share_deposit, and reservation.customer_share.balance maps to customer_share_balance; both reference care_price_catalog / accepted_reservation_quote / customer_share.

  • Payment UI entry first calls POST /reservation-payment-pre-registrations. The body contains only paymentId; bearer identity and Idempotency-Key are 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 configured clock into ReservationPaymentApi; otherwise stage returns 503 with reservation payment clock is not configured. 2026-07-08 stage browser proof frontend-repo/output/playwright/stage-payment-pre-reg-20260708163501-booking-desktop-payment-pre-registered.png covers booking creation, progress polling, charge summary, and 201 pre_registered from the payment pre-registration endpoint.

  • Kill Bill billing client operations start through POST /reservation-payment-workflow-starts and progress reads through GET /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 proof RPW-STAGE-20260708163933 completed after the DB repair migration restored sanmopia_caregiver_payout_acknowledgements and shortened the pgmq queue sanmopia_booking_term_invalidation_exec under 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-ebce7e4042cd from service delivery through caregiver payout, Branch settlement, HQ settlement, and settled. 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 table sanmopia_customer_service_balance_instruction_projections because the previous long physical table name exceeded PostgreSQL identifier length and was truncated. Proof workflow settlement-continuation:RPW-STAGE-20260708163933:service-replay-20260708170535 completed with financialStage = service_delivered; direct repair commands then advanced caregiver payout 950000, Branch settlement 1681519, HQ settlement 90000, and final settled state.

  • Kill Bill push callbacks post a generic ExtBusEvent to POST /killbill-payment-events. Payment callbacks use objectType=PAYMENT, objectId as the payment id, tenantId as tenant evidence, and eventType=PAYMENT_SUCCESS or PAYMENT_FAILED. The JSON encoded metaData supplies paymentTransactionId, transactionType, status, amount, currency, and effectiveDate. PURCHASE or CAPTURE success starts paid verification; REFUND success records refund completion; VOID success records cancellation. Failure handling also derives from transactionType. Invented provider events such as PAYMENT_REFUNDED or PAYMENT_CANCELLED are invalid.

  • Kill Bill push notifications do not include a webhook signature. KILLBILL_WEBHOOK_SECRET is 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 sends sanmopia_payment_webhook_processing through one Supabase RPC. Canonical payload replay returns duplicate without requeue. Same event identity with a different payload digest returns quarantined, appends repair evidence, and applies no business effect. Queue workers start ReservationPaymentWorkflow only for paid verification operations. 2026-07-12 refund callback proof supabase:smoke:stage:killbill-refund-callback-to-settlement-reversal posted an official-shape Kill Bill REFUND callback through the hosted route, proved duplicate replay idempotency, dispatched sanmopia_payment_webhook_processing, recorded the provider refund result from the immutable refund ledger, advanced the paid row to partial_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.sql and 20260711002100_payment_change_handoff_authorization_index_name.sql. Stage transaction proof used service_role: first command advanced adjustment and payment revisions from 0 to 1, created one prepared handoff and one audit; exact replay returned replayed=true without a second audit. Direct authenticated execution failed with permission 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-migrate runs spicedb migrate head against Supabase Postgres before authorization-dev serves traffic. authorization-schema-bootstrap writes SANMOPIA_SPICEDB_SCHEMA_PATH to 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:8443 on the internal Compose network.

  • SpiceDB HTTP is also routed through Traefik with SANMOPIA_SPICEDB_HTTP_HOST=authorization.sanmopia.localhost so stage smoke scripts can seed relationships without exposing a dedicated host port. Requests still require SANMOPIA_SPICEDB_PRESHARED_KEY.

Frontend runtime:

  • Frontend package scripts must not hardcode --host, --port, 0.0.0.0, or localhost.
  • Frontend interactive state must use Nanostores. Page scripts may bind DOM nodes, but submit/progress/payment interaction state belongs in Nanostores under src/ui/** or src/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 8a9a6f5 makes reservation-smoke-scenarios Playwright-only. No smoke settlement DTO, endpoint literal, ledger preparation, proof state, or rendering remains under production src/**; 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_HOST or ASTRO_HOST.
  • Astro dev port comes from ASTRO_DEV_PORT or ASTRO_PORT.
  • Astro preview host comes from ASTRO_PREVIEW_HOST or ASTRO_HOST.
  • Astro preview port comes from ASTRO_PREVIEW_PORT or ASTRO_PORT.
  • Compose routing still uses SANMOPIA_FRONTEND_CONTAINER_PORT and Traefik labels.

Validation:

Terminal window
pnpm stage:check
pnpm mutagen:render
docker compose --env-file .env.example -f docker-compose.yml pull assembly-static
pnpm mutagen:start
pnpm supabase:migrate
pnpm authorization:bootstrap
pnpm workflow:register
pnpm test-data:reap
pnpm supabase:seed:mother-booking-context
pnpm smoke:mother-booking-workflow
pnpm 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-dev
docker compose --env-file .env.example -f docker-compose.yml config
stat -c '%g' /var/run/docker.sock

Build and routing rules:

  • Image builds run only through .github/workflows/assembly-image-release.yml as a thin call to the organization-owned company-remote-build-v4 contract. Sanmopia owns the source, ghcr.io/yamonco/sanmopia-modernization-assembly image namespace, digest, and release lifecycle. The FractalOps executor only supplies the self-hosted runner, shared Kubernetes fractalops-buildx builder, registry cache, scan, signing, and attestation.
  • Compose and Mutagen never build images. SANMOPIA_ASSEMBLY_IMAGE is a GHCR digest produced by the release workflow; stage startup only pulls and runs it.
  • docker build, docker buildx build, Compose build, and Buildx --load are forbidden on developer hosts. Image compilation uses the FractalOps buildx-cluster-build phase only.
  • Runtime pulls are not image builds. Reuse the existing self-hosted Supabase stage; do not run supabase start or supabase stop, which would create a second CLI-managed runtime and bypass the project stage lifecycle.
  • Compose services use Traefik labels plus container expose; application ports are 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:

Terminal window
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-context

Mother booking workflow smoke:

Terminal window
pnpm authorization:bootstrap
pnpm workflow:register
pnpm test-data:reap
pnpm supabase:seed:mother-booking-context
pnpm smoke:mother-booking-workflow

Preview E2E smoke:

Terminal window
pnpm preview:e2e:smoke

This 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/SIGKILL cleanup;
  • 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:migrate records applied SQL filenames in private.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.sql as applied before replay. This prevents a false function already exists stop while preserving the canonical migration order for new stages.
  • The same drift guard records 20260702201000_reservation_caregiver_assignment_collaboration.sql when the caregiver assignment table and wrapper-chain functions already exist.

Stage recovery:

Terminal window
mutagen project -f .mutagen.project.generated.yml terminate
docker compose --env-file .env.example -f docker-compose.yml down
docker volume rm sanmopia-stage-supabase-db-data sanmopia-stage-supabase-db-config sanmopia-stage-supabase-storage-data
pnpm mutagen:render
docker compose --env-file .env.example -f docker-compose.yml pull assembly-static
pnpm mutagen:start
pnpm supabase:migrate
pnpm authorization:bootstrap
pnpm workflow:register
pnpm test-data:reap
pnpm supabase:seed:mother-booking-context
pnpm smoke:mother-booking-workflow