create-ailk 0.24.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -22
- package/component-catalog.json +3 -3
- package/component-catalog.md +3 -3
- package/dist/add-module.js +4 -2
- package/dist/add-route.d.ts +12 -1
- package/dist/add-route.js +31 -3
- package/dist/cli.js +12 -7
- package/dist/derive-architecture.d.ts +3 -4
- package/dist/derive-architecture.js +65 -29
- package/dist/lib/design-checks.d.ts +9 -4
- package/dist/lib/design-checks.js +13 -7
- package/dist/lib/extract-module-bundle.js +29 -2
- package/dist/lib/fetch-template-snapshot.d.ts +2 -1
- package/dist/lib/fetch-template-snapshot.js +8 -3
- package/dist/lib/import-graph.d.ts +1 -1
- package/dist/lib/import-graph.js +4 -4
- package/dist/lib/module-license-gate.d.ts +12 -1
- package/dist/lib/module-license-gate.js +36 -15
- package/dist/lib/module-tier.d.ts +2 -8
- package/dist/lib/module-tier.js +13 -12
- package/dist/module-architecture.d.ts +20 -3
- package/dist/module-architecture.js +129 -552
- package/dist/paid-sections.d.ts +25 -1
- package/dist/paid-sections.js +88 -3
- package/dist/starter-grid.d.ts +6 -6
- package/dist/starter-grid.js +12 -5
- package/dist/surfaces.js +39 -2
- package/dist/template-record.d.ts +21 -3
- package/dist/template-record.js +38 -7
- package/dist/topology.d.ts +16 -3
- package/dist/topology.js +54 -23
- package/dist/update.d.ts +10 -1
- package/dist/update.js +715 -21
- package/package.json +3 -3
- package/release-scope.json +2 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.json +8 -2
- package/site-templates/marketing-site/TEMPLATE-MANIFEST.md +2 -2
- package/site-templates/marketing-site/ailk-template.json +7 -1
- package/site-templates/marketing-site/provenance.json +7 -1
- package/site-templates/saas-product/ailk-template.json +2 -1
- package/site-templates/saas-product/provenance.json +1 -0
- package/templates/.claude/cloudparity-repo-hooks.json +5 -0
- package/templates/.claude/hooks/template-guard.sh +137 -0
- package/templates/.claude/rules/extending.md +106 -0
- package/templates/.claude/rules/loop-contract.md +2 -2
- package/templates/.claude/settings.json +11 -0
- package/templates/.env.example +20 -5
- package/templates/.github/workflows/ci.yml +7 -4
- package/templates/CLAUDE.md +11 -1
- package/templates/CONVENTIONS.md +8 -10
- package/templates/README.md +14 -13
- package/templates/apps/api/.env.example +23 -2
- package/templates/apps/api/CLAUDE.md +119 -83
- package/templates/apps/api/jest.config.cjs +15 -5
- package/templates/apps/api/package.json +16 -20
- package/templates/apps/api/src/__tests__/trusted-origins.server.test.ts +2 -2
- package/templates/apps/api/src/__tests__/vercel-handler.test.ts +4 -4
- package/templates/apps/api/src/index.ts +1 -1
- package/templates/apps/api/src/lib/__mocks__/prisma-client.js +16 -0
- package/templates/apps/api/src/lib/__mocks__/prisma.ts +3 -3
- package/templates/apps/api/src/modules/commerce.ts +10 -0
- package/templates/apps/api/src/modules/content-write.ts +10 -0
- package/templates/apps/api/src/modules/multi-tenant.ts +10 -0
- package/templates/apps/api/src/modules/scheduling.ts +10 -0
- package/templates/apps/api/src/modules/signal-collectors.ts +10 -0
- package/templates/apps/api/src/modules/workspace.ts +10 -0
- package/templates/apps/api/src/server.ts +117 -1882
- package/templates/apps/api/vercel.json +2 -2
- package/templates/apps/mcp/CLAUDE.md +90 -26
- package/templates/apps/mcp/__tests__/app-entry.test.ts +4 -254
- package/templates/apps/mcp/__tests__/boot-file.test.ts +92 -0
- package/templates/apps/mcp/jest.config.cjs +6 -1
- package/templates/apps/mcp/package.json +5 -13
- package/templates/apps/mcp/src/app.ts +8 -7
- package/templates/apps/mcp/src/index.ts +6 -34
- package/templates/apps/mcp/src/lib/paid-tools.generated.ts +10 -0
- package/templates/apps/mcp/src/lib/topology.generated.ts +2 -2
- package/templates/apps/mcp/src/site.ts +12 -0
- package/templates/apps/web/.env.example +1 -1
- package/templates/apps/web/CLAUDE.md +31 -3
- package/templates/apps/web/app/.well-known/mcp.json/route.ts +9 -3
- package/templates/apps/web/app/[locale]/(auth)/__tests__/auth-pages.test.tsx +43 -4
- package/templates/apps/web/app/[locale]/(auth)/legal-footnote.tsx +35 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-in/__tests__/page.test.tsx +117 -8
- package/templates/apps/web/app/[locale]/(auth)/sign-in/page.tsx +14 -37
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-card.tsx +6 -6
- package/templates/apps/web/app/[locale]/(auth)/sign-in/sign-in-text.ts +20 -35
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/page.test.tsx +108 -0
- package/templates/apps/web/app/[locale]/(auth)/sign-up/__tests__/sign-up-card.test.tsx +111 -5
- package/templates/apps/web/app/[locale]/(auth)/sign-up/page.tsx +78 -16
- package/templates/apps/web/app/[locale]/(auth)/sign-up/sign-up-card.tsx +132 -12
- package/templates/apps/web/app/[locale]/(authed)/__tests__/gate.test.tsx +53 -1
- package/templates/apps/web/app/[locale]/(authed)/account/__tests__/billing.test.tsx +38 -0
- package/templates/apps/web/app/[locale]/(authed)/account/billing/page.tsx +4 -2
- package/templates/apps/web/app/[locale]/(authed)/sign-in-gate.tsx +47 -24
- package/templates/apps/web/app/[locale]/(authed)/signed-out-gate.generated.tsx +23 -4
- package/templates/apps/web/app/[locale]/contact/intake-analytics.ts +1 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/__tests__/magnet-page.test.tsx +22 -1
- package/templates/apps/web/app/[locale]/flows/[slug]/magnet-page.tsx +7 -3
- package/templates/apps/web/app/[locale]/layout.tsx +33 -13
- package/templates/apps/web/app/[locale]/meet/confirmed/BookingHandOff.tsx +128 -0
- package/templates/apps/web/app/[locale]/meet/confirmed/__tests__/BookingHandOff.test.tsx +126 -0
- package/templates/apps/web/app/[locale]/pricing/__tests__/unconnected.test.tsx +43 -0
- package/templates/apps/web/app/[locale]/pricing/page.tsx +7 -3
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/__tests__/route.test.ts +222 -0
- package/templates/apps/web/app/api/offerings/checkout/[lookupKey]/route.ts +10 -3
- package/templates/apps/web/app/api/revalidate/route.ts +3 -3
- package/templates/apps/web/auth.ts +10 -6
- package/templates/apps/web/i18n/request.ts +49 -1
- package/templates/apps/web/jest.config.cjs +1 -0
- package/templates/apps/web/lib/__tests__/account-config.test.ts +7 -0
- package/templates/apps/web/lib/__tests__/auth-email.test.ts +276 -0
- package/templates/apps/web/lib/auth-email.ts +103 -21
- package/templates/apps/web/lib/hub-session/__tests__/sign-in-gate.test.tsx +72 -23
- package/templates/apps/web/lib/hub-session/sign-in-gate.tsx +94 -70
- package/templates/apps/web/lib/prisma-for-auth.ts +5 -2
- package/templates/apps/web/lib/relay.ts +2 -2
- package/templates/apps/web/lib/release-scope.ts +47 -0
- package/templates/apps/web/lib/server-api.ts +1 -1
- package/templates/apps/web/lib/sign-up-agreements.ts +291 -28
- package/templates/apps/web/lib/site-contributions.ts +18 -0
- package/templates/apps/web/messages/en.json +186 -20
- package/templates/apps/web/messages/site/en.json +1 -0
- package/templates/apps/web/src/lib/commerce-claim.ts +1 -1
- package/templates/customizations.yaml +37 -0
- package/templates/database/prisma.config.ts +10 -14
- package/templates/database/seed-test.ts +2 -2
- package/templates/database/seed.ts +5 -5
- package/templates/package.json +12 -6
- package/templates/pnpm-workspace.yaml +0 -1
- package/templates/project.yaml +16 -8
- package/templates/tools/jest/reporters.cjs +65 -0
- package/templates/tools/lint-architecture.ts +553 -7
- package/templates/tsconfig.json +1 -2
- package/versions.json +49 -36
- package/templates/apps/api/src/__tests__/commerce-claim-registration.test.ts +0 -409
- package/templates/apps/api/src/__tests__/factories/agent-caller.ts +0 -102
- package/templates/apps/api/src/__tests__/factories/content-fixture.ts +0 -138
- package/templates/apps/api/src/__tests__/factories/in-memory-prisma.ts +0 -441
- package/templates/apps/api/src/__tests__/factories/index.ts +0 -70
- package/templates/apps/api/src/__tests__/factories/lead-fixture.ts +0 -82
- package/templates/apps/api/src/__tests__/leads-forwarded-rate-limit.test.ts +0 -135
- package/templates/apps/api/src/__tests__/services/enrichment-shim.test.ts +0 -126
- package/templates/apps/api/src/__tests__/stripe-boot.test.ts +0 -132
- package/templates/apps/api/src/__tests__/support/hub-sign-in-fake-db.ts +0 -362
- package/templates/apps/api/src/bin/__tests__/rate-limit-probe.test.ts +0 -184
- package/templates/apps/api/src/bin/abandon-sweep.ts +0 -42
- package/templates/apps/api/src/bin/rate-limit-probe.ts +0 -53
- package/templates/apps/api/src/bin/spokes.ts +0 -198
- package/templates/apps/api/src/bin/stripe-connections-rekey.ts +0 -45
- package/templates/apps/api/src/config/__tests__/hub-require-spoke-key.test.ts +0 -196
- package/templates/apps/api/src/config/__tests__/index.test.ts +0 -233
- package/templates/apps/api/src/config/__tests__/modules.test.ts +0 -76
- package/templates/apps/api/src/config/__tests__/stripe-connection-key.test.ts +0 -185
- package/templates/apps/api/src/config/__tests__/trusted-origins.test.ts +0 -238
- package/templates/apps/api/src/config/index.ts +0 -596
- package/templates/apps/api/src/config/modules.ts +0 -83
- package/templates/apps/api/src/lib/__tests__/agent-store.test.ts +0 -581
- package/templates/apps/api/src/lib/__tests__/auth-email-budget.test.ts +0 -143
- package/templates/apps/api/src/lib/__tests__/consent-ledger-double.ts +0 -237
- package/templates/apps/api/src/lib/__tests__/consent-store.test.ts +0 -353
- package/templates/apps/api/src/lib/__tests__/content-adapter.test.ts +0 -201
- package/templates/apps/api/src/lib/__tests__/deliverable-asset-store.test.ts +0 -224
- package/templates/apps/api/src/lib/__tests__/error-serializer.test.ts +0 -120
- package/templates/apps/api/src/lib/__tests__/errors.test.ts +0 -94
- package/templates/apps/api/src/lib/__tests__/file-fallback.test.ts +0 -144
- package/templates/apps/api/src/lib/__tests__/fixtures/HomePage/home.mdx +0 -8
- package/templates/apps/api/src/lib/__tests__/flow-completion-store.test.ts +0 -149
- package/templates/apps/api/src/lib/__tests__/flow-rename-race.db.test.ts +0 -116
- package/templates/apps/api/src/lib/__tests__/hub-sign-in-store.test.ts +0 -209
- package/templates/apps/api/src/lib/__tests__/lead-capture-store.test.ts +0 -257
- package/templates/apps/api/src/lib/__tests__/lead-delivery.test.ts +0 -96
- package/templates/apps/api/src/lib/__tests__/outbound-http.test.ts +0 -125
- package/templates/apps/api/src/lib/__tests__/prisma.test.ts +0 -165
- package/templates/apps/api/src/lib/__tests__/require-commerce-claim.test.ts +0 -317
- package/templates/apps/api/src/lib/__tests__/require-paid-tier.test.ts +0 -129
- package/templates/apps/api/src/lib/__tests__/site.test.ts +0 -308
- package/templates/apps/api/src/lib/__tests__/slice-load.test.ts +0 -89
- package/templates/apps/api/src/lib/__tests__/spoke-store.test.ts +0 -202
- package/templates/apps/api/src/lib/__tests__/stripe-admission.test.ts +0 -217
- package/templates/apps/api/src/lib/__tests__/stripe.test.ts +0 -82
- package/templates/apps/api/src/lib/agent-store.ts +0 -613
- package/templates/apps/api/src/lib/auth-email-budget.ts +0 -161
- package/templates/apps/api/src/lib/consent-store.ts +0 -363
- package/templates/apps/api/src/lib/content-adapter.ts +0 -112
- package/templates/apps/api/src/lib/deliverable-asset-store.ts +0 -96
- package/templates/apps/api/src/lib/error-serializer.ts +0 -164
- package/templates/apps/api/src/lib/errors.ts +0 -95
- package/templates/apps/api/src/lib/file-fallback.ts +0 -87
- package/templates/apps/api/src/lib/flow-completion-store.ts +0 -136
- package/templates/apps/api/src/lib/hub-sign-in-store.ts +0 -455
- package/templates/apps/api/src/lib/lead-capture-store.ts +0 -122
- package/templates/apps/api/src/lib/lead-delivery.ts +0 -52
- package/templates/apps/api/src/lib/license-key-store.ts +0 -45
- package/templates/apps/api/src/lib/locales.ts +0 -201
- package/templates/apps/api/src/lib/outbound-http.ts +0 -127
- package/templates/apps/api/src/lib/prisma.ts +0 -21
- package/templates/apps/api/src/lib/proposal-paid-store.ts +0 -66
- package/templates/apps/api/src/lib/rate-limit-probe.ts +0 -165
- package/templates/apps/api/src/lib/rate-limit-store.ts +0 -142
- package/templates/apps/api/src/lib/require-commerce-claim.ts +0 -250
- package/templates/apps/api/src/lib/require-paid-tier.ts +0 -53
- package/templates/apps/api/src/lib/site.ts +0 -257
- package/templates/apps/api/src/lib/slice-load.ts +0 -90
- package/templates/apps/api/src/lib/spoke-store.ts +0 -180
- package/templates/apps/api/src/lib/stripe.ts +0 -321
- package/templates/apps/api/src/lib/webhook-idempotency.ts +0 -156
- package/templates/apps/api/src/middleware/__tests__/adversarial/bearer-token-format.test.ts +0 -111
- package/templates/apps/api/src/middleware/__tests__/adversarial/cookie-or-bearer-precedence.test.ts +0 -172
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-bearer-skip.test.ts +0 -119
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-cookie-path.test.ts +0 -138
- package/templates/apps/api/src/middleware/__tests__/adversarial/csrf-trusted-origins.test.ts +0 -142
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-fixation.test.ts +0 -155
- package/templates/apps/api/src/middleware/__tests__/adversarial/session-timeout.test.ts +0 -139
- package/templates/apps/api/src/middleware/__tests__/auth.bearer-path.test.ts +0 -562
- package/templates/apps/api/src/middleware/__tests__/auth.both-present-precedence.test.ts +0 -76
- package/templates/apps/api/src/middleware/__tests__/auth.cookie-path.test.ts +0 -220
- package/templates/apps/api/src/middleware/__tests__/auth.database-not-configured.test.ts +0 -97
- package/templates/apps/api/src/middleware/__tests__/auth.hub-session-path.test.ts +0 -557
- package/templates/apps/api/src/middleware/__tests__/auth.neither-present.test.ts +0 -72
- package/templates/apps/api/src/middleware/__tests__/auth.optional-cookie.test.ts +0 -140
- package/templates/apps/api/src/middleware/__tests__/default-tenant.test.ts +0 -721
- package/templates/apps/api/src/middleware/__tests__/rate-limit-cross-isolate.db.test.ts +0 -220
- package/templates/apps/api/src/middleware/__tests__/rate-limit-store.test.ts +0 -430
- package/templates/apps/api/src/middleware/__tests__/rate-limit.test.ts +0 -1409
- package/templates/apps/api/src/middleware/__tests__/write-budget.test.ts +0 -174
- package/templates/apps/api/src/middleware/auth.ts +0 -864
- package/templates/apps/api/src/middleware/default-tenant.ts +0 -166
- package/templates/apps/api/src/middleware/rate-limit.ts +0 -983
- package/templates/apps/api/src/openapi/__tests__/openapi.test.ts +0 -391
- package/templates/apps/api/src/openapi/__tests__/spec-identity.test.ts +0 -182
- package/templates/apps/api/src/openapi/index.ts +0 -18
- package/templates/apps/api/src/openapi/spec.ts +0 -2148
- package/templates/apps/api/src/openapi/surface-drift.ts +0 -195
- package/templates/apps/api/src/routes/aeo/__tests__/index.test.ts +0 -175
- package/templates/apps/api/src/routes/aeo/index.ts +0 -107
- package/templates/apps/api/src/routes/auth/__tests__/auth-tokens.functional.test.ts +0 -196
- package/templates/apps/api/src/routes/auth/__tests__/auth-user.functional.test.ts +0 -249
- package/templates/apps/api/src/routes/auth/__tests__/hub-sign-in.test.ts +0 -532
- package/templates/apps/api/src/routes/auth/__tests__/tokens.test.ts +0 -313
- package/templates/apps/api/src/routes/auth/__tests__/transactional-email.test.ts +0 -413
- package/templates/apps/api/src/routes/auth/__tests__/user.test.ts +0 -609
- package/templates/apps/api/src/routes/auth/hub-sign-in.ts +0 -242
- package/templates/apps/api/src/routes/auth/tokens.ts +0 -211
- package/templates/apps/api/src/routes/auth/transactional-email.ts +0 -301
- package/templates/apps/api/src/routes/auth/user.ts +0 -305
- package/templates/apps/api/src/routes/billing/plan-checkout.ts +0 -332
- package/templates/apps/api/src/routes/checkout/__tests__/live-key.test.ts +0 -178
- package/templates/apps/api/src/routes/checkout/__tests__/offerings-authorization.test.ts +0 -1426
- package/templates/apps/api/src/routes/checkout/__tests__/session.test.ts +0 -448
- package/templates/apps/api/src/routes/checkout/__tests__/sessions.test.ts +0 -1742
- package/templates/apps/api/src/routes/checkout/index.ts +0 -34
- package/templates/apps/api/src/routes/checkout/session.ts +0 -171
- package/templates/apps/api/src/routes/checkout/sessions.ts +0 -540
- package/templates/apps/api/src/routes/consent/__tests__/account.test.ts +0 -386
- package/templates/apps/api/src/routes/consent/__tests__/sign-up-rate-limit.test.ts +0 -160
- package/templates/apps/api/src/routes/consent/account.ts +0 -268
- package/templates/apps/api/src/routes/consent/cookie.ts +0 -90
- package/templates/apps/api/src/routes/consent/index.ts +0 -26
- package/templates/apps/api/src/routes/consent/unsubscribe.ts +0 -127
- package/templates/apps/api/src/routes/content/__tests__/get-page.test.ts +0 -853
- package/templates/apps/api/src/routes/content/__tests__/list-pages.test.ts +0 -230
- package/templates/apps/api/src/routes/content/__tests__/list-types.test.ts +0 -44
- package/templates/apps/api/src/routes/content/__tests__/locale.test.ts +0 -118
- package/templates/apps/api/src/routes/content/__tests__/offerings.test.ts +0 -731
- package/templates/apps/api/src/routes/content/__tests__/pricing.test.ts +0 -753
- package/templates/apps/api/src/routes/content/__tests__/rate-limit.test.ts +0 -51
- package/templates/apps/api/src/routes/content/__tests__/search.test.ts +0 -450
- package/templates/apps/api/src/routes/content/__tests__/shown-offerings.test.ts +0 -762
- package/templates/apps/api/src/routes/content/get-page.ts +0 -183
- package/templates/apps/api/src/routes/content/index.ts +0 -49
- package/templates/apps/api/src/routes/content/list-pages.ts +0 -74
- package/templates/apps/api/src/routes/content/list-types.ts +0 -19
- package/templates/apps/api/src/routes/content/offerings.ts +0 -184
- package/templates/apps/api/src/routes/content/pricing.ts +0 -176
- package/templates/apps/api/src/routes/content/rate-limit.ts +0 -22
- package/templates/apps/api/src/routes/content/search.ts +0 -100
- package/templates/apps/api/src/routes/content/shown-offerings.ts +0 -229
- package/templates/apps/api/src/routes/leads/__tests__/capture.test.ts +0 -88
- package/templates/apps/api/src/routes/leads/__tests__/list.test.ts +0 -171
- package/templates/apps/api/src/routes/leads/__tests__/magnet-completion.test.ts +0 -515
- package/templates/apps/api/src/routes/leads/__tests__/spoke.test.ts +0 -392
- package/templates/apps/api/src/routes/leads/__tests__/track.test.ts +0 -165
- package/templates/apps/api/src/routes/leads/index.ts +0 -639
- package/templates/apps/api/src/routes/leads/list.ts +0 -104
- package/templates/apps/api/src/routes/project-listings/__tests__/configured-application.test.ts +0 -626
- package/templates/apps/api/src/routes/project-listings/__tests__/drafts.test.ts +0 -1268
- package/templates/apps/api/src/routes/project-listings/__tests__/founder-identity.test.ts +0 -847
- package/templates/apps/api/src/routes/project-listings/__tests__/me.test.ts +0 -491
- package/templates/apps/api/src/routes/project-listings/__tests__/public.test.ts +0 -476
- package/templates/apps/api/src/routes/project-listings/__tests__/resume-email.test.ts +0 -872
- package/templates/apps/api/src/routes/project-listings/__tests__/site-answers.test.ts +0 -1022
- package/templates/apps/api/src/routes/project-listings/__tests__/site-key.test.ts +0 -484
- package/templates/apps/api/src/routes/project-listings/__tests__/structured-address.test.ts +0 -535
- package/templates/apps/api/src/routes/project-listings/index.ts +0 -164
- package/templates/apps/api/src/routes/project-listings/me.ts +0 -87
- package/templates/apps/api/src/routes/project-listings/patch-copy.ts +0 -224
- package/templates/apps/api/src/routes/project-listings/patch-draft.ts +0 -102
- package/templates/apps/api/src/routes/project-listings/public.ts +0 -98
- package/templates/apps/api/src/routes/project-listings/respond.ts +0 -48
- package/templates/apps/api/src/routes/project-listings/resume-token.ts +0 -27
- package/templates/apps/api/src/routes/project-listings/start.ts +0 -140
- package/templates/apps/api/src/routes/project-listings/submit.ts +0 -130
- package/templates/apps/api/src/routes/project-listings/verify-email.ts +0 -150
- package/templates/apps/api/src/routes/requests/__tests__/confirm.test.ts +0 -686
- package/templates/apps/api/src/routes/requests/confirm.ts +0 -601
- package/templates/apps/api/src/routes/requests/index.ts +0 -54
- package/templates/apps/api/src/routes/test/auth/__tests__/session.test.ts +0 -306
- package/templates/apps/api/src/routes/test/auth/session.ts +0 -143
- package/templates/apps/api/src/routes/webhooks/README.md +0 -398
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-checkout-consent.test.ts +0 -649
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-dunning.test.ts +0 -749
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-flow-checkout.test.ts +0 -449
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-idempotency.test.ts +0 -264
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode-live.test.ts +0 -159
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-livemode.test.ts +0 -158
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-offerings.test.ts +0 -814
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-org-billing.test.ts +0 -301
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-proposal-paid.test.ts +0 -369
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-rate-limit.test.ts +0 -215
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-request-paid.test.ts +0 -301
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe-site-filter.test.ts +0 -809
- package/templates/apps/api/src/routes/webhooks/__tests__/stripe.test.ts +0 -1155
- package/templates/apps/api/src/routes/webhooks/index.ts +0 -141
- package/templates/apps/api/src/routes/webhooks/stripe-offerings.ts +0 -384
- package/templates/apps/api/src/routes/webhooks/stripe.ts +0 -2028
- package/templates/apps/api/src/services/__tests__/abandon-email.test.ts +0 -645
- package/templates/apps/api/src/services/__tests__/answer-distributions.test.ts +0 -132
- package/templates/apps/api/src/services/__tests__/audit-log.test.ts +0 -1028
- package/templates/apps/api/src/services/__tests__/checkout-plan.test.ts +0 -313
- package/templates/apps/api/src/services/__tests__/consent-ledger.test.ts +0 -503
- package/templates/apps/api/src/services/__tests__/consent-migration.test.ts +0 -65
- package/templates/apps/api/src/services/__tests__/consent.test.ts +0 -299
- package/templates/apps/api/src/services/__tests__/default-tenant.test.ts +0 -147
- package/templates/apps/api/src/services/__tests__/deliverable-fulfillment.test.ts +0 -877
- package/templates/apps/api/src/services/__tests__/deliverable-resend.test.ts +0 -379
- package/templates/apps/api/src/services/__tests__/dunning-policy.test.ts +0 -368
- package/templates/apps/api/src/services/__tests__/entitlements.test.ts +0 -142
- package/templates/apps/api/src/services/__tests__/hub-sign-in.test.ts +0 -854
- package/templates/apps/api/src/services/__tests__/keyless-workspace-predicate.test.ts +0 -234
- package/templates/apps/api/src/services/__tests__/lead-accept-user.test.ts +0 -251
- package/templates/apps/api/src/services/__tests__/lead-routing.test.ts +0 -2660
- package/templates/apps/api/src/services/__tests__/listing-config.test.ts +0 -166
- package/templates/apps/api/src/services/__tests__/magnet-completion.test.ts +0 -389
- package/templates/apps/api/src/services/__tests__/person.test.ts +0 -1204
- package/templates/apps/api/src/services/__tests__/plan-offerings.test.ts +0 -450
- package/templates/apps/api/src/services/__tests__/project-listings.counts.test.ts +0 -202
- package/templates/apps/api/src/services/__tests__/project-listings.fence.test.ts +0 -151
- package/templates/apps/api/src/services/__tests__/public-site-resolver.test.ts +0 -137
- package/templates/apps/api/src/services/__tests__/request-confirm.test.ts +0 -531
- package/templates/apps/api/src/services/__tests__/revalidation.test.ts +0 -372
- package/templates/apps/api/src/services/__tests__/slack-notify.test.ts +0 -348
- package/templates/apps/api/src/services/__tests__/spoke-resolution.test.ts +0 -660
- package/templates/apps/api/src/services/__tests__/usage-aggregation.test.ts +0 -941
- package/templates/apps/api/src/services/__tests__/usage-credits.test.ts +0 -588
- package/templates/apps/api/src/services/__tests__/verified-email.test.ts +0 -78
- package/templates/apps/api/src/services/abandon-email.ts +0 -336
- package/templates/apps/api/src/services/aeo-score.ts +0 -325
- package/templates/apps/api/src/services/answer-distributions.ts +0 -138
- package/templates/apps/api/src/services/audit-log.ts +0 -961
- package/templates/apps/api/src/services/checkout-buyer.ts +0 -168
- package/templates/apps/api/src/services/consent.ts +0 -834
- package/templates/apps/api/src/services/default-tenant.ts +0 -416
- package/templates/apps/api/src/services/deliverable-fulfillment.ts +0 -750
- package/templates/apps/api/src/services/dunning-policy.ts +0 -294
- package/templates/apps/api/src/services/enrichment-shim.ts +0 -53
- package/templates/apps/api/src/services/entitlements.ts +0 -338
- package/templates/apps/api/src/services/hub-sign-in.ts +0 -472
- package/templates/apps/api/src/services/lead-accept-user.ts +0 -126
- package/templates/apps/api/src/services/lead-promotion.ts +0 -160
- package/templates/apps/api/src/services/lead-routing.ts +0 -1372
- package/templates/apps/api/src/services/leads-inbox.ts +0 -312
- package/templates/apps/api/src/services/listing-config.ts +0 -178
- package/templates/apps/api/src/services/magnet-completion.ts +0 -242
- package/templates/apps/api/src/services/offerings/__tests__/catalog.test.ts +0 -348
- package/templates/apps/api/src/services/offerings/__tests__/checkout-session-record.test.ts +0 -268
- package/templates/apps/api/src/services/offerings/__tests__/connection.test.ts +0 -975
- package/templates/apps/api/src/services/offerings/__tests__/fixtures.ts +0 -416
- package/templates/apps/api/src/services/offerings/__tests__/org-products.test.ts +0 -400
- package/templates/apps/api/src/services/offerings/__tests__/readers.test.ts +0 -685
- package/templates/apps/api/src/services/offerings/__tests__/source.test.ts +0 -231
- package/templates/apps/api/src/services/offerings/catalog.ts +0 -318
- package/templates/apps/api/src/services/offerings/checkout-authorization.ts +0 -403
- package/templates/apps/api/src/services/offerings/checkout-session-record.ts +0 -192
- package/templates/apps/api/src/services/offerings/connection.ts +0 -1083
- package/templates/apps/api/src/services/offerings/index.ts +0 -614
- package/templates/apps/api/src/services/offerings/source.ts +0 -140
- package/templates/apps/api/src/services/offerings/store.ts +0 -150
- package/templates/apps/api/src/services/owner-workspaces.ts +0 -132
- package/templates/apps/api/src/services/person.ts +0 -1211
- package/templates/apps/api/src/services/plan-offerings.ts +0 -125
- package/templates/apps/api/src/services/project-listing-resume-email.ts +0 -326
- package/templates/apps/api/src/services/project-listings.ts +0 -2085
- package/templates/apps/api/src/services/public-site-resolver.ts +0 -396
- package/templates/apps/api/src/services/request-confirm.ts +0 -698
- package/templates/apps/api/src/services/revalidation.ts +0 -235
- package/templates/apps/api/src/services/slack-notify.ts +0 -291
- package/templates/apps/api/src/services/usage-aggregation.ts +0 -516
- package/templates/apps/api/src/services/usage-credits.ts +0 -340
- package/templates/apps/api/src/services/verified-email.ts +0 -71
- package/templates/apps/mcp/__tests__/abuse/daily-cap.test.ts +0 -52
- package/templates/apps/mcp/__tests__/abuse/email-validator.test.ts +0 -46
- package/templates/apps/mcp/__tests__/abuse/guard.test.ts +0 -198
- package/templates/apps/mcp/__tests__/abuse/honeypot.test.ts +0 -36
- package/templates/apps/mcp/__tests__/abuse/user-agent-deny.test.ts +0 -54
- package/templates/apps/mcp/__tests__/aeo-cli-parity.test.ts +0 -116
- package/templates/apps/mcp/__tests__/audit-single-page.test.ts +0 -299
- package/templates/apps/mcp/__tests__/boot.test.ts +0 -280
- package/templates/apps/mcp/__tests__/catalog-drift.test.ts +0 -359
- package/templates/apps/mcp/__tests__/config/modules.test.ts +0 -78
- package/templates/apps/mcp/__tests__/get_business_hours.test.ts +0 -82
- package/templates/apps/mcp/__tests__/get_faq.test.ts +0 -80
- package/templates/apps/mcp/__tests__/get_locations.test.ts +0 -65
- package/templates/apps/mcp/__tests__/get_offerings.test.ts +0 -557
- package/templates/apps/mcp/__tests__/get_pricing.test.ts +0 -524
- package/templates/apps/mcp/__tests__/get_site_info.test.ts +0 -81
- package/templates/apps/mcp/__tests__/http-transport.test.ts +0 -1232
- package/templates/apps/mcp/__tests__/index.test.ts +0 -195
- package/templates/apps/mcp/__tests__/lib/audit-single-page-readiness-scan.test.ts +0 -191
- package/templates/apps/mcp/__tests__/lib/commerce-claim.test.ts +0 -531
- package/templates/apps/mcp/__tests__/lib/content-adapter-parity.test.ts +0 -70
- package/templates/apps/mcp/__tests__/lib/content-adapter.test.ts +0 -162
- package/templates/apps/mcp/__tests__/lib/content-root.test.ts +0 -93
- package/templates/apps/mcp/__tests__/lib/errors.test.ts +0 -75
- package/templates/apps/mcp/__tests__/lib/fail-closed-tools.test.ts +0 -180
- package/templates/apps/mcp/__tests__/lib/public-workspace.test.ts +0 -124
- package/templates/apps/mcp/__tests__/list_pages.test.ts +0 -268
- package/templates/apps/mcp/__tests__/locale.test.ts +0 -120
- package/templates/apps/mcp/__tests__/module-exclusion.test.ts +0 -175
- package/templates/apps/mcp/__tests__/purchase-disabled.test.ts +0 -101
- package/templates/apps/mcp/__tests__/rate-limit-write.test.ts +0 -189
- package/templates/apps/mcp/__tests__/read_content.test.ts +0 -511
- package/templates/apps/mcp/__tests__/request_callback.test.ts +0 -160
- package/templates/apps/mcp/__tests__/search_content.test.ts +0 -271
- package/templates/apps/mcp/__tests__/server.test.ts +0 -388
- package/templates/apps/mcp/__tests__/start_purchase.test.ts +0 -448
- package/templates/apps/mcp/__tests__/submit_lead.test.ts +0 -222
- package/templates/apps/mcp/__tests__/subscribe_newsletter.test.ts +0 -152
- package/templates/apps/mcp/__tests__/tools/create_project.test.ts +0 -243
- package/templates/apps/mcp/__tests__/tools/purchase.test.ts +0 -451
- package/templates/apps/mcp/src/abuse/daily-cap.ts +0 -47
- package/templates/apps/mcp/src/abuse/email-validator.ts +0 -25
- package/templates/apps/mcp/src/abuse/honeypot.ts +0 -21
- package/templates/apps/mcp/src/abuse/index.ts +0 -349
- package/templates/apps/mcp/src/abuse/user-agent-deny.ts +0 -58
- package/templates/apps/mcp/src/boot.ts +0 -167
- package/templates/apps/mcp/src/config/modules.ts +0 -43
- package/templates/apps/mcp/src/http-transport.ts +0 -564
- package/templates/apps/mcp/src/lib/audit-single-page.ts +0 -308
- package/templates/apps/mcp/src/lib/commerce-claim.ts +0 -263
- package/templates/apps/mcp/src/lib/content-adapter.ts +0 -116
- package/templates/apps/mcp/src/lib/empty-content-adapter.ts +0 -140
- package/templates/apps/mcp/src/lib/errors.ts +0 -61
- package/templates/apps/mcp/src/lib/license-key-store.ts +0 -45
- package/templates/apps/mcp/src/lib/locales.ts +0 -171
- package/templates/apps/mcp/src/lib/prisma.ts +0 -39
- package/templates/apps/mcp/src/lib/public-workspace.ts +0 -133
- package/templates/apps/mcp/src/lib/resolve-locale.ts +0 -30
- package/templates/apps/mcp/src/lib/shown-offerings.ts +0 -105
- package/templates/apps/mcp/src/lib/site-config.ts +0 -53
- package/templates/apps/mcp/src/lib/stripe-tax-signal.ts +0 -59
- package/templates/apps/mcp/src/request-context.ts +0 -52
- package/templates/apps/mcp/src/server.ts +0 -343
- package/templates/apps/mcp/src/serverless-app.ts +0 -88
- package/templates/apps/mcp/src/tools/audit.ts +0 -127
- package/templates/apps/mcp/src/tools/capture_lead.ts +0 -66
- package/templates/apps/mcp/src/tools/create_project.ts +0 -134
- package/templates/apps/mcp/src/tools/get_business_hours.ts +0 -68
- package/templates/apps/mcp/src/tools/get_faq.ts +0 -71
- package/templates/apps/mcp/src/tools/get_locations.ts +0 -88
- package/templates/apps/mcp/src/tools/get_offerings.ts +0 -96
- package/templates/apps/mcp/src/tools/get_pricing.ts +0 -94
- package/templates/apps/mcp/src/tools/get_site_info.ts +0 -86
- package/templates/apps/mcp/src/tools/index.ts +0 -138
- package/templates/apps/mcp/src/tools/list_pages.ts +0 -68
- package/templates/apps/mcp/src/tools/person_notes.ts +0 -25
- package/templates/apps/mcp/src/tools/proposals.ts +0 -27
- package/templates/apps/mcp/src/tools/purchase.ts +0 -205
- package/templates/apps/mcp/src/tools/read_content.ts +0 -81
- package/templates/apps/mcp/src/tools/request_callback.ts +0 -52
- package/templates/apps/mcp/src/tools/search_content.ts +0 -63
- package/templates/apps/mcp/src/tools/subscribe_newsletter.ts +0 -54
- package/templates/apps/mcp/src/workspace-server.ts +0 -224
- package/templates/database/CHANGELOG.md +0 -981
- package/templates/database/LICENSE +0 -202
- package/templates/database/NOTICE +0 -2
- package/templates/database/README.md +0 -243
- package/templates/database/__tests__/agents.db.test.ts +0 -320
- package/templates/database/__tests__/appointment.db.test.ts +0 -106
- package/templates/database/__tests__/artifact-content-store.db.test.ts +0 -316
- package/templates/database/__tests__/composite-fence.db.test.ts +0 -206
- package/templates/database/__tests__/consent-ledger.db.test.ts +0 -345
- package/templates/database/__tests__/consent-source-checkout-migration.test.ts +0 -79
- package/templates/database/__tests__/fence-fan-out.db.test.ts +0 -715
- package/templates/database/__tests__/fixtures/appointment-create-probe.ts +0 -79
- package/templates/database/__tests__/fixtures/person-backfill.json +0 -264
- package/templates/database/__tests__/fixtures/person-contact-backfill.json +0 -182
- package/templates/database/__tests__/fixtures/project-listing-consent-probe.ts +0 -100
- package/templates/database/__tests__/flow-completion-migration.test.ts +0 -115
- package/templates/database/__tests__/flow-completion.db.test.ts +0 -136
- package/templates/database/__tests__/flow-workspace-key.db.test.ts +0 -263
- package/templates/database/__tests__/flows-listing-diagnostic.test.ts +0 -392
- package/templates/database/__tests__/flows-presets.test.ts +0 -173
- package/templates/database/__tests__/in-page-magnets-migration.test.ts +0 -161
- package/templates/database/__tests__/lead-idempotency-key.db.test.ts +0 -92
- package/templates/database/__tests__/member-area.db.test.ts +0 -257
- package/templates/database/__tests__/member-contract-migration.test.ts +0 -85
- package/templates/database/__tests__/member-contract.db.test.ts +0 -413
- package/templates/database/__tests__/offerings-checkout-session.db.test.ts +0 -270
- package/templates/database/__tests__/offerings-migration.test.ts +0 -197
- package/templates/database/__tests__/offerings-shown-price-ids-migration.test.ts +0 -86
- package/templates/database/__tests__/offerings.db.test.ts +0 -303
- package/templates/database/__tests__/outbound-message-action.db.test.ts +0 -80
- package/templates/database/__tests__/person-contact.db.test.ts +0 -563
- package/templates/database/__tests__/person-notes.db.test.ts +0 -249
- package/templates/database/__tests__/person.db.test.ts +0 -586
- package/templates/database/__tests__/piece-kind-migration.test.ts +0 -67
- package/templates/database/__tests__/piece-kind.db.test.ts +0 -208
- package/templates/database/__tests__/prefixed-ids.db.test.ts +0 -222
- package/templates/database/__tests__/project-listing-consent-at.db.test.ts +0 -111
- package/templates/database/__tests__/seed.test.ts +0 -272
- package/templates/database/__tests__/spoke-registry.db.test.ts +0 -191
- package/templates/database/__tests__/tenancy.test.ts +0 -141
- package/templates/database/__tests__/trial-magnet-migration.test.ts +0 -186
- package/templates/database/__tests__/user-cascade.test.ts +0 -140
- package/templates/database/_gitignore +0 -8
- package/templates/database/auth/.gitkeep +0 -0
- package/templates/database/auth/schema.prisma +0 -641
- package/templates/database/config/.gitkeep +0 -0
- package/templates/database/config/schema.prisma +0 -441
- package/templates/database/content/.gitkeep +0 -0
- package/templates/database/content/schema.prisma +0 -339
- package/templates/database/eslint.config.mjs +0 -69
- package/templates/database/flows/listing-diagnostic.ts +0 -182
- package/templates/database/flows/presets.ts +0 -315
- package/templates/database/inbox/.gitkeep +0 -0
- package/templates/database/inbox/README.md +0 -3
- package/templates/database/inbox/__tests__/schema.test.ts +0 -44
- package/templates/database/inbox/schema.prisma +0 -1447
- package/templates/database/jest.config.cjs +0 -36
- package/templates/database/migrations/20260508000000_add_inbox_namespace/migration.sql +0 -328
- package/templates/database/migrations/20260513000000_better_auth_migration/migration.sql +0 -100
- package/templates/database/migrations/20260515000000_add_user_billing_fields/migration.sql +0 -28
- package/templates/database/migrations/20260516000000_add_user_tier_care_plan_seat/migration.sql +0 -25
- package/templates/database/migrations/20260518000000_add_telemetry_records/migration.sql +0 -42
- package/templates/database/migrations/20260525000000_theme_wiring/migration.sql +0 -67
- package/templates/database/migrations/20260710000000_retire_commercial_tier/migration.sql +0 -21
- package/templates/database/migrations/20260712215647_add_multi_tenant_tenancy/migration.sql +0 -201
- package/templates/database/migrations/20260713143320_reanchor_site_config_to_workspace/migration.sql +0 -67
- package/templates/database/migrations/20260713174000_add_audit_logs/migration.sql +0 -46
- package/templates/database/migrations/20260713190000_drop_telemetry_records/migration.sql +0 -25
- package/templates/database/migrations/20260713192000_add_uuidv7_function/migration.sql +0 -49
- package/templates/database/migrations/20260713192429_prefixed_uuid_entity_ids/migration.sql +0 -197
- package/templates/database/migrations/20260713200000_add_org_billing/migration.sql +0 -32
- package/templates/database/migrations/20260714000000_add_lead_promotion_fields/migration.sql +0 -22
- package/templates/database/migrations/20260714010000_add_usage_billing_content_models/migration.sql +0 -151
- package/templates/database/migrations/20260714020000_add_usage_billing_auth_config_models/migration.sql +0 -223
- package/templates/database/migrations/20260714030000_credit_balance_non_negative/migration.sql +0 -22
- package/templates/database/migrations/20260714030000_widen_usage_billing_report_aggregates/migration.sql +0 -32
- package/templates/database/migrations/20260810233516_add_flow_engine_models/migration.sql +0 -40
- package/templates/database/migrations/20260811020000_add_flow_session_abandon_email_columns/migration.sql +0 -4
- package/templates/database/migrations/20260811030000_add_flow_session_abandon_email_index/migration.sql +0 -2
- package/templates/database/migrations/20260811040000_add_flow_session_segment/migration.sql +0 -2
- package/templates/database/migrations/20260811050000_add_deliverable_models/migration.sql +0 -29
- package/templates/database/migrations/20260812000000_add_deliverable_grant_send_columns/migration.sql +0 -6
- package/templates/database/migrations/20260902222948_add_waitlist_experiment_models/migration.sql +0 -39
- package/templates/database/migrations/20260902224500_add_waitlist_id_format_checks/migration.sql +0 -24
- package/templates/database/migrations/20260903004112_add_flow_session_flags_aeo_score/migration.sql +0 -3
- package/templates/database/migrations/20260903010000_add_playbook_instance/migration.sql +0 -33
- package/templates/database/migrations/20260904000000_add_waitlist_followup_send/migration.sql +0 -30
- package/templates/database/migrations/20260905230000_add_flow_checkout/migration.sql +0 -47
- package/templates/database/migrations/20260906000000_add_flow_checkout_idempotency/migration.sql +0 -16
- package/templates/database/migrations/20260907120000_add_project_listing/migration.sql +0 -47
- package/templates/database/migrations/20260907130000_add_project_listing_slack_columns/migration.sql +0 -10
- package/templates/database/migrations/20260907180000_add_listing_csv_send/migration.sql +0 -26
- package/templates/database/migrations/20260909120000_project_listings_platform/migration.sql +0 -90
- package/templates/database/migrations/20260909180000_listing_draft_and_fields/migration.sql +0 -174
- package/templates/database/migrations/20260910120000_listing_public_site_key/migration.sql +0 -14
- package/templates/database/migrations/20260911120000_listing_copy_edit_audit/migration.sql +0 -25
- package/templates/database/migrations/20260911140000_listing_structured_address/migration.sql +0 -32
- package/templates/database/migrations/20260911180000_consent_grants/migration.sql +0 -71
- package/templates/database/migrations/20260911200000_listing_site_answers/migration.sql +0 -30
- package/templates/database/migrations/20260912120000_listing_owner_link/migration.sql +0 -49
- package/templates/database/migrations/20260914120000_listing_founder_identity/migration.sql +0 -123
- package/templates/database/migrations/20260915120000_project_listing_solution/migration.sql +0 -15
- package/templates/database/migrations/20260916120000_artifact_content_store/migration.sql +0 -158
- package/templates/database/migrations/20260917120000_callback_request_confirm/migration.sql +0 -40
- package/templates/database/migrations/20260918120000_add_appointment/migration.sql +0 -67
- package/templates/database/migrations/20260918120000_asset_metadata_columns/migration.sql +0 -70
- package/templates/database/migrations/20260919120000_flow_workspace_key/migration.sql +0 -181
- package/templates/database/migrations/20260919130000_flow_definition_admin_columns/migration.sql +0 -172
- package/templates/database/migrations/20260919130000_workspace_meetings/migration.sql +0 -41
- package/templates/database/migrations/20260919140000_inbox_workspace_key/migration.sql +0 -185
- package/templates/database/migrations/20260920120000_lead_handled_at/migration.sql +0 -70
- package/templates/database/migrations/20260920130000_lead_workspace_source_index/migration.sql +0 -57
- package/templates/database/migrations/20260921120000_deliverable_grant_workspace_key/migration.sql +0 -68
- package/templates/database/migrations/20260922120000_tenancy_fence/migration.sql +0 -1067
- package/templates/database/migrations/20260923120000_listing_capturing_magnet/migration.sql +0 -210
- package/templates/database/migrations/20261003120000_spoke_registry/migration.sql +0 -80
- package/templates/database/migrations/20261003130000_scheduling_connection/migration.sql +0 -49
- package/templates/database/migrations/20261005120000_agents/migration.sql +0 -84
- package/templates/database/migrations/20261008120000_user_last_workspace/migration.sql +0 -35
- package/templates/database/migrations/20261008130000_lead_idempotency_key/migration.sql +0 -20
- package/templates/database/migrations/20261009120000_agent_grants/migration.sql +0 -33
- package/templates/database/migrations/20261010120000_person/migration.sql +0 -106
- package/templates/database/migrations/20261010121000_person_backfill/migration.sql +0 -132
- package/templates/database/migrations/20261010130000_agent_grants_people_read/migration.sql +0 -20
- package/templates/database/migrations/20261011120000_member_area/migration.sql +0 -158
- package/templates/database/migrations/20261011130000_agent_grants_member_pieces_manage/migration.sql +0 -21
- package/templates/database/migrations/20261012120000_member_contract/migration.sql +0 -192
- package/templates/database/migrations/20261013120000_lead_spoke_checkout/migration.sql +0 -20
- package/templates/database/migrations/20261014120000_consent_ledger/migration.sql +0 -122
- package/templates/database/migrations/20261014120000_person_contact/migration.sql +0 -231
- package/templates/database/migrations/20261015120000_person_notes/migration.sql +0 -77
- package/templates/database/migrations/20261015120000_rate_limit_counters/migration.sql +0 -43
- package/templates/database/migrations/20261016120000_piece_kind/migration.sql +0 -95
- package/templates/database/migrations/20261017120000_outbound_message_action/migration.sql +0 -30
- package/templates/database/migrations/20261018120000_flow_completion/migration.sql +0 -52
- package/templates/database/migrations/20261019120000_consent_source_checkout/migration.sql +0 -27
- package/templates/database/migrations/20261020120000_offerings_data_model/migration.sql +0 -146
- package/templates/database/migrations/20261021120000_offerings_checkout_session/migration.sql +0 -77
- package/templates/database/migrations/20261022120000_in_page_magnets/migration.sql +0 -45
- package/templates/database/migrations/20261023120000_offerings_shown_price_ids/migration.sql +0 -47
- package/templates/database/migrations/20261024120000_organization_plan_checkout_session/migration.sql +0 -16
- package/templates/database/migrations/20261025120000_trial_magnet/migration.sql +0 -28
- package/templates/database/migrations/20261026120000_trial_magnet_proof_line/migration.sql +0 -23
- package/templates/database/migrations/migration_lock.toml +0 -3
- package/templates/database/ops/.gitkeep +0 -0
- package/templates/database/ops/schema.prisma +0 -43
- package/templates/database/package.json +0 -89
- package/templates/database/schema.prisma +0 -71
- package/templates/database/scripts/db-generate-locked.sh +0 -80
- package/templates/database/scripts/person-backfill.sql +0 -132
- package/templates/database/tenancy.ts +0 -183
- package/templates/database/tsconfig.build.json +0 -16
- package/templates/database/tsconfig.flows.json +0 -15
- package/templates/database/tsconfig.json +0 -16
- package/templates/database/tsconfig.tenancy.json +0 -14
|
@@ -1,1447 +0,0 @@
|
|
|
1
|
-
// inbox namespace — Lead, Subscriber, Callback
|
|
2
|
-
//
|
|
3
|
-
// Backs the Phase B anonymous-write MCP tools (submit_lead, subscribe_newsletter,
|
|
4
|
-
// request_callback) and their corresponding POST routes (B6).
|
|
5
|
-
//
|
|
6
|
-
// Per ADR 0009 D2: no @relation directives cross namespace boundaries.
|
|
7
|
-
// siteId is a plain String identifier — no FK to auth/users.
|
|
8
|
-
//
|
|
9
|
-
// Generator + datasource blocks live in database/schema.prisma (the root schema
|
|
10
|
-
// file). This file holds only model definitions for the inbox namespace.
|
|
11
|
-
//
|
|
12
|
-
// ── The fence fan-out (#6374) — why `workspaceId` sits where it does ────────
|
|
13
|
-
// Eleven models in this file carry the tenancy fence: a NOT NULL
|
|
14
|
-
// `workspaceId` mapped to `workspace_id`, an `@@index([workspaceId])`, and a
|
|
15
|
-
// `<table>_workspace_site_agree` CHECK added by
|
|
16
|
-
// database/migrations/20260922120000_tenancy_fence. No Prisma relation is
|
|
17
|
-
// declared for it (no Prisma relation directive) and no raw-SQL foreign key
|
|
18
|
-
// is added (ADR-0009 invariant #1) — the CHECK is the fence, and it is
|
|
19
|
-
// strictly tighter than an FK, because `site_id` IS the resolved workspace id.
|
|
20
|
-
|
|
21
|
-
/// Lead — anonymous lead submission (Phase B submit_lead tool / B6 route)
|
|
22
|
-
model Lead {
|
|
23
|
-
id String @id @default(dbgenerated("('lead_'::text || (uuidv7())::text)")) @db.Text
|
|
24
|
-
siteId String @map("site_id")
|
|
25
|
-
// #6203 — the isolation key `tenantDb(tenant).leads` filters on. Plain
|
|
26
|
-
// scalar, no @relation (ADR 0009 D1, the same reasoning `promotedUserId`
|
|
27
|
-
// below already applies). Backfilled by
|
|
28
|
-
// database/migrations/20260919140000_inbox_workspace_key.
|
|
29
|
-
//
|
|
30
|
-
// #6254 AC-3/AC-4/AC-7 — NOT NULL as of
|
|
31
|
-
// database/migrations/20260922120000_tenancy_fence, which also normalizes
|
|
32
|
-
// `siteId` to the resolved workspace id and adds
|
|
33
|
-
// `leads_workspace_site_agree CHECK ("workspace_id" = "site_id")`: since
|
|
34
|
-
// `siteId` IS the workspace id (never a WorkspaceSiteIdentity id — see
|
|
35
|
-
// that model's docblock), Postgres itself now rejects a row whose two
|
|
36
|
-
// columns disagree, in place of the issue's literal (and unbuildable,
|
|
37
|
-
// ADR-0009-crossing) composite FK — docs/database/namespaces.md. Every
|
|
38
|
-
// write site derives this value server-side from the resolved site
|
|
39
|
-
// alias; never from a request body (AC-6).
|
|
40
|
-
workspaceId String @map("workspace_id")
|
|
41
|
-
source String
|
|
42
|
-
email String
|
|
43
|
-
payload Json
|
|
44
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
45
|
-
|
|
46
|
-
// #6204 — the triage stamp. NOT `promotedAt`'s sibling: `promotedAt` below
|
|
47
|
-
// is the lead→user PROMOTION stamp, written by the email-verification
|
|
48
|
-
// reconciler and true of a lead regardless of whether anyone has answered
|
|
49
|
-
// it. `handledAt` is written by an operator (or an agent acting for one)
|
|
50
|
-
// clearing a MESSAGE out of the Needs-you queue (spec D8). Nullable, no
|
|
51
|
-
// default — every existing row reads NULL, i.e. unhandled (spec D5).
|
|
52
|
-
// `handledBy` is the actor, mirroring `ProjectListing.decidedBy` below
|
|
53
|
-
// (spec D4/S-7) — plain scalar, no @relation (ADR 0009 D2), the same
|
|
54
|
-
// posture `promotedUserId` already takes.
|
|
55
|
-
handledAt DateTime? @map("handled_at")
|
|
56
|
-
handledBy String? @map("handled_by")
|
|
57
|
-
|
|
58
|
-
// Lead → user promotion (#3586). Plain scalars — no cross-namespace
|
|
59
|
-
// @relation into auth.User (ADR 0009 invariant #1). Set by
|
|
60
|
-
// apps/api/src/services/lead-promotion.ts once the reconciling user's
|
|
61
|
-
// email is verified and the lead's siteId resolves to one of that user's
|
|
62
|
-
// own workspaces (tenant-safety, spec D3).
|
|
63
|
-
promotedUserId String? @map("promoted_user_id")
|
|
64
|
-
promotedAt DateTime? @map("promoted_at")
|
|
65
|
-
|
|
66
|
-
// #7869 (roadmap D5) — the caller's `Idempotency-Key` header on
|
|
67
|
-
// `POST /v1/leads/submit`, stored verbatim so a retried write finds the
|
|
68
|
-
// first lead. Written only by `apps/api/src/services/lead-routing.ts`'s
|
|
69
|
-
// submit branch and read only by its two scoped lookups. Untrusted,
|
|
70
|
-
// client-supplied text — never echo it. Nullable, no default: every
|
|
71
|
-
// pre-#7869 row and every keyless lead reads NULL, and Postgres treats
|
|
72
|
-
// NULLs as distinct under the unique below. Same shape as
|
|
73
|
-
// `FlowCheckout.idempotencyKey` (#4912).
|
|
74
|
-
idempotencyKey String? @map("idempotency_key")
|
|
75
|
-
|
|
76
|
-
// #852 D1 — the Person this lead belongs to: one client of this workspace,
|
|
77
|
-
// stamped at creation by `apps/api/src/services/lead-routing.ts` through
|
|
78
|
-
// `findOrCreatePerson`. Plain scalar, no @relation (the inbox house style,
|
|
79
|
-
// `FlowCheckout.leadId`). NULL only when the email is not a storable
|
|
80
|
-
// address or the Person step failed — a Person step never refuses a lead.
|
|
81
|
-
// Backfilled by database/migrations/20261010121000_person_backfill.
|
|
82
|
-
personId String? @map("person_id")
|
|
83
|
-
|
|
84
|
-
// #8185 D1 — the hub's conclusion, at write time, that this lead is a
|
|
85
|
-
// spoke's LIVE paid checkout: the submit carried a registered spoke's key,
|
|
86
|
-
// was not relayed for a visitor, and passed the #7877 checkout plan rule's
|
|
87
|
-
// own predicate (a `cs_live_` Idempotency-Key and the checkout shape).
|
|
88
|
-
// Written only by `apps/api/src/services/lead-routing.ts`'s submit create
|
|
89
|
-
// and never updated; read only by `apps/api/src/lib/member-store.ts`'s
|
|
90
|
-
// `listMemberPurchases`, which serves a marked lead as a purchase. A lead
|
|
91
|
-
// written before the column existed reads false: `viaSpoke` was never
|
|
92
|
-
// stored, so its provenance cannot be proven (no backfill). No index: the
|
|
93
|
-
// read filters by the already-indexed `personId`.
|
|
94
|
-
spokeCheckout Boolean @default(false) @map("spoke_checkout")
|
|
95
|
-
|
|
96
|
-
// #7869 — the idempotency guarantee, scoped per workspace so a key can
|
|
97
|
-
// never reach another tenant's row.
|
|
98
|
-
@@unique([workspaceId, idempotencyKey])
|
|
99
|
-
@@index([siteId])
|
|
100
|
-
@@index([createdAt])
|
|
101
|
-
@@index([email])
|
|
102
|
-
@@index([workspaceId])
|
|
103
|
-
// #6204 — the message-inbox read: workspaceId = $1 AND handledAt IS NULL
|
|
104
|
-
// AND source = ANY($2), ordered by createdAt desc. Two columns, not three
|
|
105
|
-
// (spec D10): `source` is an IN over four values Postgres applies cheaply
|
|
106
|
-
// after this narrowing, so it is not worth widening the index on every
|
|
107
|
-
// write.
|
|
108
|
-
@@index([workspaceId, handledAt])
|
|
109
|
-
// #6246 — the per-magnet read: workspaceId = $1 AND source = $2, ordered
|
|
110
|
-
// by createdAt desc (spec § 1.5/D1). `source` here is an EXACT-equality
|
|
111
|
-
// `'flow:' + <magnet slug>` value, not the `handledAt` index's allowlist
|
|
112
|
-
// `IN` — a distinct query shape that composite does not serve.
|
|
113
|
-
@@index([workspaceId, source])
|
|
114
|
-
@@index([personId])
|
|
115
|
-
@@map("leads")
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/// Person — one client of one workspace (#852). Created only by
|
|
119
|
-
/// apps/api/src/services/person.ts's findOrCreatePerson. Plain scalars, no
|
|
120
|
-
/// @relation (ADR 0009): workspaceId and userId name rows in the auth
|
|
121
|
-
/// namespace.
|
|
122
|
-
///
|
|
123
|
-
/// `email` is the lowercased, trimmed address (the `people_email_normalized`
|
|
124
|
-
/// CHECK holds it to printable ASCII). `userId` is written only on a verified
|
|
125
|
-
/// email match (`stampPersonUser`) and never overwritten. `billingCustomerId`
|
|
126
|
-
/// is NOT unique — the spokes share one Stripe account — and the first
|
|
127
|
-
/// writer wins. `planOffering` is written by a later phase (Epic 2's
|
|
128
|
-
/// checkout-lead rule); this slice only carries the column.
|
|
129
|
-
model Person {
|
|
130
|
-
id String @id @default(dbgenerated("('person_'::text || (uuidv7())::text)")) @db.Text
|
|
131
|
-
workspaceId String @map("workspace_id")
|
|
132
|
-
email String @db.VarChar(254)
|
|
133
|
-
name String? @db.VarChar(200)
|
|
134
|
-
userId String? @map("user_id")
|
|
135
|
-
billingProvider String? @map("billing_provider") @db.VarChar(32)
|
|
136
|
-
billingCustomerId String? @map("billing_customer_id") @db.VarChar(255)
|
|
137
|
-
planOffering String? @map("plan_offering") @db.VarChar(100)
|
|
138
|
-
// #8435 D1 — contact details, to the standard (option A). Every column is
|
|
139
|
-
// nullable: a capture fills only a blank one (D4), an edit overwrites (D9,
|
|
140
|
-
// D11). The address is the magnet's six parts, stored whole or not at all
|
|
141
|
-
// (people_address_complete).
|
|
142
|
-
givenName String? @map("given_name") @db.VarChar(100)
|
|
143
|
-
familyName String? @map("family_name") @db.VarChar(100)
|
|
144
|
-
telephone String? @db.VarChar(16)
|
|
145
|
-
addressLine1 String? @map("address_line1") @db.VarChar(100)
|
|
146
|
-
addressLine2 String? @map("address_line2") @db.VarChar(60)
|
|
147
|
-
addressCity String? @map("address_city") @db.VarChar(60)
|
|
148
|
-
addressRegion String? @map("address_region") @db.VarChar(40)
|
|
149
|
-
addressPostalCode String? @map("address_postal_code") @db.VarChar(20)
|
|
150
|
-
addressCountry String? @map("address_country") @db.Char(2)
|
|
151
|
-
companyName String? @map("company_name") @db.VarChar(200)
|
|
152
|
-
jobTitle String? @map("job_title") @db.VarChar(100)
|
|
153
|
-
// Who last saved the contact by hand — "operator" (the Person page) or
|
|
154
|
-
// "client" (their account) — and when. NULL until a human saves it; while
|
|
155
|
-
// NULL and `userId` is NULL, a capture may fill a blank field (D4).
|
|
156
|
-
contactConfirmedAt DateTime? @map("contact_confirmed_at")
|
|
157
|
-
contactConfirmedBy String? @map("contact_confirmed_by") @db.VarChar(16)
|
|
158
|
-
// #8514 D1 — the team's private note on this client: one per Person, never
|
|
159
|
-
// served to the client. Who last saved it (a user id of the auth namespace,
|
|
160
|
-
// no foreign key — ADR 0009) and when; both NULL until the first save.
|
|
161
|
-
// When an agent saved it, which agent (its person is notesUpdatedBy).
|
|
162
|
-
notes String? @db.VarChar(5000)
|
|
163
|
-
notesUpdatedAt DateTime? @map("notes_updated_at")
|
|
164
|
-
notesUpdatedBy String? @map("notes_updated_by")
|
|
165
|
-
notesUpdatedByAgent String? @map("notes_updated_by_agent")
|
|
166
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
167
|
-
updatedAt DateTime @updatedAt @map("updated_at")
|
|
168
|
-
|
|
169
|
-
pieces Piece[]
|
|
170
|
-
interactions Interaction[]
|
|
171
|
-
callbacks Callback[]
|
|
172
|
-
subscribers Subscriber[]
|
|
173
|
-
|
|
174
|
-
@@unique([workspaceId, email])
|
|
175
|
-
@@unique([workspaceId, userId])
|
|
176
|
-
// #7877 D1 — redundant with the PK, load-bearing as the target of the
|
|
177
|
-
// composite (person_id, workspace_id) FKs from pieces and interactions:
|
|
178
|
-
// "a Person of the same workspace" is a database fact.
|
|
179
|
-
@@unique([id, workspaceId])
|
|
180
|
-
@@index([workspaceId, billingCustomerId])
|
|
181
|
-
@@index([workspaceId, createdAt])
|
|
182
|
-
@@map("people")
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/// Piece — one deliverable or proposal for one client (#7877, #8516). Written only through
|
|
186
|
-
/// tenantDb(tenant).memberArea; read by a client only through
|
|
187
|
-
/// lib/member-store.ts. `stage` is one of the workspace's declared stages
|
|
188
|
-
/// (WorkspacePieceStages), checked at write time, never an enum.
|
|
189
|
-
///
|
|
190
|
-
/// #8168 D5 — `approvedAt` is set in the same transaction as an `approved`
|
|
191
|
-
/// review of the latest PieceVersion and freezes review: once set, no further
|
|
192
|
-
/// review lands. Approval is a piece-level fact; the version's own `review`
|
|
193
|
-
/// records which version was approved.
|
|
194
|
-
model Piece {
|
|
195
|
-
id String @id @default(dbgenerated("('piece_'::text || (uuidv7())::text)")) @db.Text
|
|
196
|
-
workspaceId String @map("workspace_id")
|
|
197
|
-
personId String @map("person_id")
|
|
198
|
-
title String @db.VarChar(200)
|
|
199
|
-
stage String @db.VarChar(40)
|
|
200
|
-
needsClient Boolean @default(false) @map("needs_client")
|
|
201
|
-
updatedBy String @map("updated_by")
|
|
202
|
-
approvedAt DateTime? @map("approved_at")
|
|
203
|
-
// #8516 D1 — what the piece is: a `deliverable` (every row before #8516) or a
|
|
204
|
-
// `proposal` the client approves or sends back. Never changes after create.
|
|
205
|
-
kind String @default("deliverable") @db.VarChar(16)
|
|
206
|
-
// A proposal's price, in minor units of a two-decimal currency; both or
|
|
207
|
-
// neither, and only on a proposal.
|
|
208
|
-
priceAmount Int? @map("price_amount")
|
|
209
|
-
priceCurrency String? @map("price_currency") @db.Char(3)
|
|
210
|
-
// The client's current Stripe Checkout Session (#8516 D11) — its id, URL and
|
|
211
|
-
// expiry. Never served to the operator. Replaced once five minutes or less
|
|
212
|
-
// remain; the replaced session is expired at Stripe.
|
|
213
|
-
checkoutSessionId String? @unique @map("checkout_session_id")
|
|
214
|
-
checkoutUrl String? @map("checkout_url") @db.VarChar(2048)
|
|
215
|
-
checkoutExpiresAt DateTime? @map("checkout_expires_at")
|
|
216
|
-
// When Stripe reported the proposal paid, and the session that paid it (D13).
|
|
217
|
-
paidAt DateTime? @map("paid_at")
|
|
218
|
-
paidSessionId String? @map("paid_session_id")
|
|
219
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
220
|
-
updatedAt DateTime @updatedAt @map("updated_at")
|
|
221
|
-
|
|
222
|
-
person Person @relation(fields: [personId, workspaceId], references: [id, workspaceId], onDelete: Restrict, map: "pieces_person_fk")
|
|
223
|
-
versions PieceVersion[]
|
|
224
|
-
|
|
225
|
-
@@index([workspaceId, personId, createdAt])
|
|
226
|
-
// #8168 D1 — redundant with the PK, load-bearing as the target of
|
|
227
|
-
// piece_versions_piece_fk: a version names a piece of THIS Person in THIS
|
|
228
|
-
// workspace, as one database fact.
|
|
229
|
-
@@unique([id, personId, workspaceId])
|
|
230
|
-
@@map("pieces")
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/// PieceVersion — one delivery of a piece to its client (#8168 D1, D2): a
|
|
234
|
-
/// file (`assetId`, a private library asset of the same workspace) or a link
|
|
235
|
-
/// (`link`, https only), never both, numbered from 1 per piece. The client's
|
|
236
|
-
/// review of it (`review`, `reviewComment`, `reviewedAt`) is recorded on the
|
|
237
|
-
/// version itself. There is NO round count, limit or remaining-count column,
|
|
238
|
-
/// by design (#8140 disposition 3).
|
|
239
|
-
///
|
|
240
|
-
/// Its one relation is the three-column FK onto `pieces(id, person_id,
|
|
241
|
-
/// workspace_id)`, so the piece, its Person and the workspace are all one
|
|
242
|
-
/// database fact. `assetId` and `deliveredBy` name rows of other namespaces:
|
|
243
|
-
/// plain columns with format CHECKs, no foreign key (ADR 0009).
|
|
244
|
-
model PieceVersion {
|
|
245
|
-
id String @id @default(dbgenerated("('pver_'::text || (uuidv7())::text)")) @db.Text
|
|
246
|
-
workspaceId String @map("workspace_id")
|
|
247
|
-
personId String @map("person_id")
|
|
248
|
-
pieceId String @map("piece_id")
|
|
249
|
-
version Int
|
|
250
|
-
assetId String? @map("asset_id")
|
|
251
|
-
link String? @db.VarChar(2048)
|
|
252
|
-
note String? @db.Text
|
|
253
|
-
deliveredBy String @map("delivered_by")
|
|
254
|
-
deliveredAt DateTime @default(now()) @map("delivered_at")
|
|
255
|
-
review String? @db.VarChar(24)
|
|
256
|
-
reviewComment String? @map("review_comment") @db.Text
|
|
257
|
-
reviewedAt DateTime? @map("reviewed_at")
|
|
258
|
-
|
|
259
|
-
piece Piece @relation(fields: [pieceId, personId, workspaceId], references: [id, personId, workspaceId], onDelete: Restrict, map: "piece_versions_piece_fk")
|
|
260
|
-
|
|
261
|
-
@@unique([pieceId, version])
|
|
262
|
-
// A client's artifacts: workspace_id = $1 AND person_id = $2 ORDER BY delivered_at DESC.
|
|
263
|
-
@@index([workspaceId, personId, deliveredAt])
|
|
264
|
-
// The public asset read's "is this asset a version file" check (AC-MAA-21).
|
|
265
|
-
@@index([assetId])
|
|
266
|
-
@@map("piece_versions")
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/// Interaction — one entry on a client's record (#7877): the original CRM
|
|
270
|
-
/// spec's Interaction cut to one channel. `readAt` is when a member read
|
|
271
|
-
/// FIRST served the row to the client's spoke — a delivery stamp, not proof a
|
|
272
|
-
/// human read it.
|
|
273
|
-
///
|
|
274
|
-
/// #8168 D3/D4 — `direction` is `outbound` (an operator's message, #7877) or
|
|
275
|
-
/// `inbound` (a client's own write); `kind` is `message`, `quote` or
|
|
276
|
-
/// `review`. An outbound row is always a `message` whose `payload` is null
|
|
277
|
-
/// or, since #8515 D6, exactly `{ "action": "onboarding" }` — the one outbound
|
|
278
|
-
/// action, rendered by the client's site as a link to `/account/onboarding`
|
|
279
|
-
/// (`interactions_outbound_shape`); a `quote` or `review` row carries the
|
|
280
|
-
/// stored payload `packages/validation`'s member.ts types. Every existing
|
|
281
|
-
/// reader filters to `direction: "outbound"`, so an inbound row is invisible
|
|
282
|
-
/// to them until a reader opts in.
|
|
283
|
-
model Interaction {
|
|
284
|
-
id String @id @default(dbgenerated("('intx_'::text || (uuidv7())::text)")) @db.Text
|
|
285
|
-
workspaceId String @map("workspace_id")
|
|
286
|
-
personId String @map("person_id")
|
|
287
|
-
channel String @db.VarChar(16)
|
|
288
|
-
direction String @db.VarChar(16)
|
|
289
|
-
kind String @default("message") @db.VarChar(16)
|
|
290
|
-
subject String @db.VarChar(200)
|
|
291
|
-
body String @db.Text
|
|
292
|
-
payload Json?
|
|
293
|
-
assetId String? @map("asset_id")
|
|
294
|
-
createdBy String @map("created_by")
|
|
295
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
296
|
-
readAt DateTime? @map("read_at")
|
|
297
|
-
|
|
298
|
-
person Person @relation(fields: [personId, workspaceId], references: [id, workspaceId], onDelete: Restrict, map: "interactions_person_fk")
|
|
299
|
-
|
|
300
|
-
@@index([workspaceId, personId, createdAt])
|
|
301
|
-
@@index([assetId])
|
|
302
|
-
@@map("interactions")
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/// Subscriber — newsletter subscription (Phase B subscribe_newsletter tool / B6 route)
|
|
306
|
-
model Subscriber {
|
|
307
|
-
id String @id @default(dbgenerated("('subscr_'::text || (uuidv7())::text)")) @db.Text
|
|
308
|
-
siteId String @map("site_id")
|
|
309
|
-
// #6374 — the tenancy fence, extended to this model (spec
|
|
310
|
-
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
311
|
-
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
312
|
-
// subscribers_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
313
|
-
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
314
|
-
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
315
|
-
// value, never one from a request body (AC-6).
|
|
316
|
-
workspaceId String @map("workspace_id")
|
|
317
|
-
email String
|
|
318
|
-
source String
|
|
319
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
320
|
-
// #8435 D1/D5 — the Person this subscription belongs to; composite FK onto
|
|
321
|
-
// people(id, workspace_id), the #7877 shape. NULL is not checked.
|
|
322
|
-
personId String? @map("person_id")
|
|
323
|
-
person Person? @relation(fields: [personId, workspaceId], references: [id, workspaceId], onDelete: Restrict, map: "subscribers_person_fk")
|
|
324
|
-
|
|
325
|
-
@@unique([siteId, email])
|
|
326
|
-
@@index([siteId])
|
|
327
|
-
@@index([personId])
|
|
328
|
-
@@index([workspaceId])
|
|
329
|
-
@@map("subscribers")
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/// Callback — callback request (Phase B request_callback tool / B6 route),
|
|
333
|
-
/// plus the request-and-confirm lifecycle (#5970, D-67 / SUB-100).
|
|
334
|
-
///
|
|
335
|
-
/// The eleven trailing members are the confirm/pay lifecycle. They are
|
|
336
|
-
/// deliberately ON THIS MODEL rather than on a second `ServiceRequest` row
|
|
337
|
-
/// (spec #5970 D1), and the cost of that choice is stated rather than hidden:
|
|
338
|
-
/// every ORDINARY callback row — the overwhelming majority — now carries
|
|
339
|
-
/// eleven NULL columns it will never use. The alternative duplicates
|
|
340
|
-
/// siteId/name/phone/preferredTime/payload or carries a cross-row join for a
|
|
341
|
-
/// one-to-one relationship, and it puts a SECOND write inside the anonymous
|
|
342
|
-
/// lead-capture path, which is a never-gate surface. Eleven nullable columns
|
|
343
|
-
/// is the cheaper side.
|
|
344
|
-
model Callback {
|
|
345
|
-
id String @id @default(dbgenerated("('callbk_'::text || (uuidv7())::text)")) @db.Text
|
|
346
|
-
siteId String @map("site_id")
|
|
347
|
-
// #6374 — the tenancy fence, extended to this model (spec
|
|
348
|
-
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
349
|
-
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
350
|
-
// callbacks_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
351
|
-
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
352
|
-
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
353
|
-
// value, never one from a request body (AC-6).
|
|
354
|
-
workspaceId String @map("workspace_id")
|
|
355
|
-
name String
|
|
356
|
-
phone String
|
|
357
|
-
preferredTime String? @map("preferred_time")
|
|
358
|
-
payload Json
|
|
359
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
360
|
-
|
|
361
|
-
// ── request-and-confirm lifecycle (#5970) ──────────────────────────────
|
|
362
|
-
/// sha-256 hex digest of the tenant's confirm token. The token itself is
|
|
363
|
-
/// emailed once and never stored; NULLED when the credential is spent,
|
|
364
|
-
/// which is the single-use mechanism. `@unique` mirrors `resumeTokenHash` —
|
|
365
|
-
/// a digest collision is a bug, not a state.
|
|
366
|
-
confirmTokenHash String? @unique @map("confirm_token_hash")
|
|
367
|
-
/// When the credential stops resolving (mint + 7 days).
|
|
368
|
-
confirmTokenExpiresAt DateTime? @map("confirm_token_expires_at")
|
|
369
|
-
/// Stamped by the tenant's confirm POST. Also the "already confirmed" predicate.
|
|
370
|
-
confirmedAt DateTime? @map("confirmed_at")
|
|
371
|
-
/// What the tenant agreed to, as they typed it — a string like
|
|
372
|
-
/// `preferredTime` above, not a parsed instant.
|
|
373
|
-
confirmedTime String? @map("confirmed_time")
|
|
374
|
-
/// Where the hosted payment URL is sent. Typed on the confirm form,
|
|
375
|
-
/// prefilled from `payload.email` when the capture form carried one.
|
|
376
|
-
buyerEmail String? @map("buyer_email")
|
|
377
|
-
/// MINOR units, never major — Stripe's own `unit_amount` convention.
|
|
378
|
-
amountMinor Int? @map("amount_minor")
|
|
379
|
-
/// Lowercase ISO-4217.
|
|
380
|
-
currency String?
|
|
381
|
-
/// The Payment Link the confirm step minted — THE artifact id the webhook
|
|
382
|
-
/// stamp matches on (`session.payment_link`).
|
|
383
|
-
stripePaymentLinkId String? @map("stripe_payment_link_id")
|
|
384
|
-
/// The session written from the same event, for support lookups. NOT a
|
|
385
|
-
/// match key, which is why it carries no index.
|
|
386
|
-
stripeCheckoutSessionId String? @map("stripe_checkout_session_id")
|
|
387
|
-
/// NULL-forever at #5970's boundary — present so the hosted-invoice rung
|
|
388
|
-
/// lands additively, with no second migration.
|
|
389
|
-
stripeInvoiceId String? @map("stripe_invoice_id")
|
|
390
|
-
/// Stamped exactly once by the Stripe webhook; the `paidAt: null` guard on
|
|
391
|
-
/// that update IS the durable idempotency key.
|
|
392
|
-
paidAt DateTime? @map("paid_at")
|
|
393
|
-
|
|
394
|
-
// #8435 D1/D5 — the Person this request belongs to (its payload.email, or
|
|
395
|
-
// the buyer email at confirm). Composite FK onto people(id, workspace_id):
|
|
396
|
-
// "a Person of the same workspace" is a database fact. NULL is not checked.
|
|
397
|
-
personId String? @map("person_id")
|
|
398
|
-
person Person? @relation(fields: [personId, workspaceId], references: [id, workspaceId], onDelete: Restrict, map: "callbacks_person_fk")
|
|
399
|
-
|
|
400
|
-
@@index([siteId])
|
|
401
|
-
@@index([personId])
|
|
402
|
-
@@index([createdAt])
|
|
403
|
-
@@index([stripePaymentLinkId])
|
|
404
|
-
@@index([stripeInvoiceId])
|
|
405
|
-
@@index([workspaceId])
|
|
406
|
-
@@map("callbacks")
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/// FlowDefinition — server-owned flow config, one live definition per
|
|
410
|
-
/// (siteId, slug) (#3956 D2). No versioning at this layer — a session
|
|
411
|
-
/// snapshots the config at start (see FlowSession.configSnapshot), so a
|
|
412
|
-
/// mid-flight definition edit never corrupts a live session.
|
|
413
|
-
model FlowDefinition {
|
|
414
|
-
id String @id @default(dbgenerated("('flow_'::text || (uuidv7())::text)")) @db.Text
|
|
415
|
-
siteId String @map("site_id")
|
|
416
|
-
/// #6155 AD-2 — the workspace isolation key. Backfilled from `siteId`: the
|
|
417
|
-
/// literal "default" resolves to the seeded sentinel workspace
|
|
418
|
-
/// (wksp_00000000-0000-7000-8000-000000000001); any other value resolves
|
|
419
|
-
/// only by exact `workspaces.id` match. Plain string, no `@relation`: ADR
|
|
420
|
-
/// 0009 forbids a Prisma relation across the inbox -> auth (and, per
|
|
421
|
-
/// #6254, inbox -> config) namespace boundary, the same reason
|
|
422
|
-
/// `Asset.workspaceId` (database/content/schema.prisma) carries none.
|
|
423
|
-
///
|
|
424
|
-
/// #6254 AC-3/AC-4/AC-7 — NOT NULL as of
|
|
425
|
-
/// database/migrations/20260922120000_tenancy_fence, which also
|
|
426
|
-
/// normalizes `siteId` to the resolved workspace id and adds
|
|
427
|
-
/// `flow_definitions_workspace_site_agree CHECK ("workspace_id" =
|
|
428
|
-
/// "site_id")` — see `Lead.workspaceId` above for the full rationale
|
|
429
|
-
/// (identical here). Every write site derives this value server-side;
|
|
430
|
-
/// never from a request body (AC-6).
|
|
431
|
-
workspaceId String @map("workspace_id")
|
|
432
|
-
slug String
|
|
433
|
-
config Json
|
|
434
|
-
/// #6156 D1/D3 — the magnet's classification, derived structurally (never
|
|
435
|
-
/// from slug) by `deriveFlowKind` on CREATE only: special-report |
|
|
436
|
-
/// diagnostic | listicle | waitlist | contact | newsletter. Keeps its
|
|
437
|
-
/// column default ('contact') — unlike `status`, a mis-classified magnet
|
|
438
|
-
/// is a mislabel, not a publication or security control.
|
|
439
|
-
kind String @default("contact")
|
|
440
|
-
/// #6156 D1 — the operator-facing name; the slug is not a name. NOT NULL
|
|
441
|
-
/// with NO column default (D1): a nameless magnet must never enter the
|
|
442
|
-
/// table silently, so every future writer is required by `tsc` to state
|
|
443
|
-
/// one, exactly as `status` below is required to state a status.
|
|
444
|
-
title String
|
|
445
|
-
/// #6156 D1/D4 — an operator-declared publish address, site-relative only
|
|
446
|
-
/// (`flowLandingPageSchema`, packages/validation/src/schemas/flow.ts).
|
|
447
|
-
/// NULL means "no operator-declared landing page; the canonical
|
|
448
|
-
/// `/{locale}/flows/{slug}` route serves it" — never a derived guess, since
|
|
449
|
-
/// a magnet has THREE public addresses (the indexed landing route, the
|
|
450
|
-
/// walk route one segment deeper, and the results route), and only the
|
|
451
|
-
/// operator can say which one they are publishing.
|
|
452
|
-
landingPage String? @db.Text @map("landing_page")
|
|
453
|
-
/// #6156 D1/D2/D10 — draft | live. NOT NULL with NO column default (D1,
|
|
454
|
-
/// SEC-08): silence must never publish, so every future writer is required
|
|
455
|
-
/// by `tsc` to name a status. Existing rows are backfilled 'live' by the
|
|
456
|
-
/// migration (every row that exists today is already serving). The
|
|
457
|
-
/// `flow_definitions_status_domain` CHECK (D2) closes the deny-list gate in
|
|
458
|
-
/// `resolveSession` (D10): an unknown value is unstorable, so "not
|
|
459
|
-
/// 'draft'" and "'live'" are the same set at the database level.
|
|
460
|
-
status String
|
|
461
|
-
// #5036 D6d — the listing this definition was projected from, when it was.
|
|
462
|
-
// Null for every flow that is not a listing diagnostic. Before this column
|
|
463
|
-
// the only link was that the generated flow's slug HAPPENED to equal the
|
|
464
|
-
// listing's slug, which nothing enforced; a slug collision would have
|
|
465
|
-
// attributed a submission to the wrong founder. ADR 0009 forbids a relation
|
|
466
|
-
// across the namespace boundary, so this is a plain string — but a stated
|
|
467
|
-
// one rather than a coincidence.
|
|
468
|
-
listingId String? @map("listing_id")
|
|
469
|
-
/// #8168 D7 — whether a signed-in client may submit this magnet from the
|
|
470
|
-
/// member area. A column, not a config block, because a spoke seed refuses
|
|
471
|
-
/// non-seedable config (#7876); `upsertFlowDefinition` never names it, so a
|
|
472
|
-
/// seed never changes it. Independent of `status`: the seeded
|
|
473
|
-
/// `request-quote` magnet is `draft` (never served anonymously) with
|
|
474
|
-
/// `customersEnabled = true`.
|
|
475
|
-
customersEnabled Boolean @default(false) @map("customers_enabled")
|
|
476
|
-
/// #8757 D1/D2 — what happens after someone submits the magnet: `lead`
|
|
477
|
-
/// (the thank-you only), `invite` (an account link with the email
|
|
478
|
-
/// prefilled) or `checkout` (continue to `checkoutOffering`). A column, on
|
|
479
|
-
/// the `customersEnabled` footing: no seed or config names it. Closed by
|
|
480
|
-
/// the `flow_definitions_completion_domain` CHECK; the standard defaults are
|
|
481
|
-
/// the migration's backfill by slug (`STANDARD_MAGNET_COMPLETIONS`).
|
|
482
|
-
completion String @default("lead") @db.VarChar(16)
|
|
483
|
-
/// #8757 D1/D2 — the offering key a `checkout` magnet continues to; NULL
|
|
484
|
-
/// exactly when `completion` is not `checkout`
|
|
485
|
-
/// (`flow_definitions_checkout_offering_pair`), in the spoke checkout slug
|
|
486
|
-
/// format (`flow_definitions_checkout_offering_format`).
|
|
487
|
-
checkoutOffering String? @map("checkout_offering") @db.VarChar(40)
|
|
488
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
489
|
-
updatedAt DateTime @updatedAt @map("updated_at")
|
|
490
|
-
|
|
491
|
-
@@unique([siteId, slug])
|
|
492
|
-
@@index([workspaceId])
|
|
493
|
-
@@map("flow_definitions")
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
/// FlowSession — a flow walk in progress or completed; the server-side
|
|
497
|
-
/// partial-lead record (#3956 D2/D9). `configSnapshot` pins the definition at
|
|
498
|
-
/// session start; `state` accumulates each landed step's values immediately
|
|
499
|
-
/// on submission (the per-step persistence this feature exists for).
|
|
500
|
-
/// `email` is denormalized from the landed answers the moment a
|
|
501
|
-
/// `mapsTo:'email'` field lands, so the #3962 abandon-email query
|
|
502
|
-
/// (`email != null AND completedAt == null`) is one indexed read.
|
|
503
|
-
model FlowSession {
|
|
504
|
-
id String @id @default(dbgenerated("('flsess_'::text || (uuidv7())::text)")) @db.Text
|
|
505
|
-
siteId String @map("site_id")
|
|
506
|
-
/// #6155 AD-2 — the workspace isolation key, same backfill contract as
|
|
507
|
-
/// `FlowDefinition.workspaceId` above; #6254 makes it NOT NULL plus the
|
|
508
|
-
/// same `flow_sessions_workspace_site_agree` equality CHECK — see
|
|
509
|
-
/// `Lead.workspaceId` (database/inbox/schema.prisma, top of file) for the
|
|
510
|
-
/// full rationale. Declared here, immediately after `siteId` and before
|
|
511
|
-
/// `state`'s empty-object-literal default below: the drift guard's model
|
|
512
|
-
/// regex (`tests/architecture/tenant-scope-pattern.test.ts`) truncates a
|
|
513
|
-
/// model's captured body at its first closing brace — including one
|
|
514
|
-
/// inside a comment or a string literal, not only in code — so a field
|
|
515
|
-
/// placed after that point (or a stray brace pair written ABOVE it, as
|
|
516
|
-
/// this very note must avoid) would be invisible to it.
|
|
517
|
-
workspaceId String @map("workspace_id")
|
|
518
|
-
// Informational only — never joined on (ADR 0009 D1: logical, not FK,
|
|
519
|
-
// isolation). Session resumption + completion key on flowSlug, not this
|
|
520
|
-
// column; deliberate, per code-review finding on #3956's acceptance PR.
|
|
521
|
-
flowId String @map("flow_id")
|
|
522
|
-
flowSlug String @map("flow_slug")
|
|
523
|
-
configSnapshot Json @map("config_snapshot")
|
|
524
|
-
state Json @default("{}")
|
|
525
|
-
email String?
|
|
526
|
-
// The session's locale (#3962 D3) — set once at create from the first
|
|
527
|
-
// landed step's `input.locale ?? configSnapshot.defaultLocale`; not
|
|
528
|
-
// updated on later steps. Null on pre-migration rows (no backfill; the
|
|
529
|
-
// abandon-email sweep falls back to the live config's defaultLocale).
|
|
530
|
-
locale String?
|
|
531
|
-
// Server-derived segment (#3959 D3) — set from the segmentation step's
|
|
532
|
-
// band map at landing time, persisted in the same write as `state`. No
|
|
533
|
-
// index: #3960/#3961/#3962 read it off rows they already fetch by
|
|
534
|
-
// existing indexes (subtraction-first, spec Clarifications).
|
|
535
|
-
segment String?
|
|
536
|
-
// D2 (#4663) — every derived flag, re-derived and persisted at each
|
|
537
|
-
// landing (`{ [flagName]: true }`). `{}` on a snapshot declaring no
|
|
538
|
-
// `flags` (the regression floor — byte-identical to a pre-#4663 row).
|
|
539
|
-
flags Json @default("{}")
|
|
540
|
-
// D6 (#4663) — the mid-flow aeo_score step's persisted result (`ready` /
|
|
541
|
-
// `unavailable`). Null until the step lands, or after a URL change resets
|
|
542
|
-
// it (D3). Never `computing` — that status never reaches the wire from
|
|
543
|
-
// this repo (packages/validation/src/schemas/aeo-score.ts).
|
|
544
|
-
aeoScore Json? @map("aeo_score")
|
|
545
|
-
leadId String? @map("lead_id")
|
|
546
|
-
// #5036 D6d — copied from the FlowDefinition at session create, so a
|
|
547
|
-
// submission is attributable to ONE listing without going through the slug.
|
|
548
|
-
// Null for every session on a flow that is not a listing diagnostic.
|
|
549
|
-
listingId String? @map("listing_id")
|
|
550
|
-
completedAt DateTime? @map("completed_at")
|
|
551
|
-
// Abandon-email audit stamp (#3962 D5) — set at most once by the sweep,
|
|
552
|
-
// per outcome ('sent' | 'dark' | 'failed'). The `abandonEmailStatus ==
|
|
553
|
-
// null` selection term is what makes the sweep idempotent across any
|
|
554
|
-
// number of overlapping runs; 'failed' is terminal (no auto-retry).
|
|
555
|
-
abandonEmailStatus String? @map("abandon_email_status")
|
|
556
|
-
abandonEmailAt DateTime? @map("abandon_email_at")
|
|
557
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
558
|
-
updatedAt DateTime @updatedAt @map("updated_at")
|
|
559
|
-
|
|
560
|
-
@@index([siteId])
|
|
561
|
-
@@index([siteId, completedAt])
|
|
562
|
-
@@index([workspaceId])
|
|
563
|
-
@@index([email])
|
|
564
|
-
// Backs the abandon-email sweep's per-definition selection query
|
|
565
|
-
// (apps/api/src/services/abandon-email.ts — flowId equality + completedAt
|
|
566
|
-
// null + abandonEmailStatus null + updatedAt window), run once per
|
|
567
|
-
// FlowDefinition on every sweep — code-review finding on #3962's
|
|
568
|
-
// acceptance PR (without this, that filter set is a full table scan).
|
|
569
|
-
@@index([flowId, completedAt, abandonEmailStatus])
|
|
570
|
-
// #5036 D6d — backs the listing-to-submissions read the explicit reference
|
|
571
|
-
// exists to make possible.
|
|
572
|
-
@@index([listingId])
|
|
573
|
-
@@map("flow_sessions")
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/// DeliverableAsset — LEGACY (#6310 D3/D11). An operator-uploaded asset the
|
|
577
|
-
/// funnel's `deliverables` config block references by `key` (#3961 D1).
|
|
578
|
-
/// Bytes live IN THE DATABASE (Postgres bytea) behind one internal
|
|
579
|
-
/// resolver — zero new infrastructure; reversibility is structural (a later
|
|
580
|
-
/// blob-storage swap changes only the resolver + the upload CLI, never the
|
|
581
|
-
/// config contract, grant model, or email link shape). Uploaded via the
|
|
582
|
-
/// `deliverable:upload` CLI (apps/api/src/bin/deliverable-upload.ts) only —
|
|
583
|
-
/// no HTTP upload route.
|
|
584
|
-
///
|
|
585
|
-
/// Since #6310: a magnet deliverable resolves against the workspace-scoped
|
|
586
|
-
/// `Asset` model (database/content/schema.prisma) FIRST; this store is the
|
|
587
|
-
/// FALLBACK, tried only on an `Asset` miss (D2). No existing row is
|
|
588
|
-
/// migrated — D3 grounds why in the two server-internal write/read paths —
|
|
589
|
-
/// so every row here keeps delivering, unmigrated, until an operator
|
|
590
|
-
/// confirms none remain (§2.3's exit condition).
|
|
591
|
-
model DeliverableAsset {
|
|
592
|
-
id String @id @default(dbgenerated("('dasset_'::text || (uuidv7())::text)")) @db.Text
|
|
593
|
-
siteId String @map("site_id")
|
|
594
|
-
// #6374 — the tenancy fence, extended to this model (spec
|
|
595
|
-
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
596
|
-
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
597
|
-
// deliverable_assets_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
598
|
-
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
599
|
-
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
600
|
-
// value, never one from a request body (AC-6).
|
|
601
|
-
workspaceId String @map("workspace_id")
|
|
602
|
-
// The assetKey a flow config's deliverables.assets entry references.
|
|
603
|
-
key String
|
|
604
|
-
// Download filename (Content-Disposition). Stored RAW at upload time
|
|
605
|
-
// (the `deliverable:upload` CLI writes the operator's `--filename` or the
|
|
606
|
-
// uploaded file's own basename verbatim) — sanitization to a safe charset
|
|
607
|
-
// happens at SERVE time only, in the download route's `sanitizeFilename`
|
|
608
|
-
// (apps/api/src/routes/deliverables/index.ts, #4002). Never trust this
|
|
609
|
-
// column as pre-sanitized; the serve-time guard is load-bearing, not
|
|
610
|
-
// redundant.
|
|
611
|
-
filename String
|
|
612
|
-
contentType String @map("content_type")
|
|
613
|
-
bytes Bytes
|
|
614
|
-
size Int
|
|
615
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
616
|
-
updatedAt DateTime @updatedAt @map("updated_at")
|
|
617
|
-
|
|
618
|
-
@@unique([siteId, key])
|
|
619
|
-
@@index([workspaceId])
|
|
620
|
-
@@map("deliverable_assets")
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/// DeliverableGrant — the capability token minted per fulfilled lead (#3961
|
|
624
|
-
/// D5). The grant's id IS the token: the download route
|
|
625
|
-
/// (GET /v1/deliverables/:grantId) looks up bytes by grant id alone, so
|
|
626
|
-
/// nothing is publicly enumerable by key, filename, or any other guessable
|
|
627
|
-
/// value — only the uuidv7-random grant id, delivered once via the lead's
|
|
628
|
-
/// own email. `assetId`/`leadId` are stable-id refs (ADR 0009 invariant 1
|
|
629
|
-
/// posture) — no cross-model `@relation`.
|
|
630
|
-
///
|
|
631
|
-
/// `assetId`'s three id classes (#6310): `asset_` (a workspace `Asset`,
|
|
632
|
-
/// scoped serve via `workspaceId` below), `dasset_` (legacy
|
|
633
|
-
/// `DeliverableAsset`, scoped by `siteId` above), `playbook:` (the FEAT-074
|
|
634
|
-
/// U4b virtual asset — no store row at all).
|
|
635
|
-
model DeliverableGrant {
|
|
636
|
-
id String @id @default(dbgenerated("('dgrant_'::text || (uuidv7())::text)")) @db.Text
|
|
637
|
-
siteId String @map("site_id")
|
|
638
|
-
assetId String @map("asset_id")
|
|
639
|
-
leadId String @map("lead_id")
|
|
640
|
-
sessionId String @map("session_id")
|
|
641
|
-
/// The workspace that authorized this grant's asset resolution (#6310
|
|
642
|
-
/// D15). Plain string, no
|
|
643
|
-
/// @relation (ADR 0009 invariant 1) — the same shape
|
|
644
|
-
/// FlowDefinition.workspaceId carries. The serve path reads it ONLY for
|
|
645
|
-
/// the `asset_` class, and a NULL there is a 404, never an unscoped read.
|
|
646
|
-
///
|
|
647
|
-
/// #6374 CHANGED WHO WRITES IT, AND THE SAFETY ARGUMENT MOVED WITH IT.
|
|
648
|
-
/// Before #6374 this was written ONLY when `assetId` was an `asset_` id,
|
|
649
|
-
/// and stayed NULL for a legacy `dasset_` grant and a `playbook:` virtual
|
|
650
|
-
/// one. As of #6374 BOTH mint sites stamp it unconditionally
|
|
651
|
-
/// (services/deliverable-fulfillment.ts, services/listing-csv-sweep.ts),
|
|
652
|
-
/// so NO newly-minted grant is NULL and the fail-closed sentinel below
|
|
653
|
-
/// now protects PRE-EXISTING rows only — the migration still touches none
|
|
654
|
-
/// of them.
|
|
655
|
-
///
|
|
656
|
-
/// That is safe because of a DIFFERENT invariant than the one that used to
|
|
657
|
-
/// carry it, and the new one is more fragile, so it is written down here
|
|
658
|
-
/// rather than left implicit. What makes stamping safe is NOT "the column
|
|
659
|
-
/// is NULL on legacy grants" — it no longer is. It is that
|
|
660
|
-
/// routes/deliverables/index.ts branches on the `assetId` PREFIX, and only
|
|
661
|
-
/// the `asset_` arm ever reads this column: the `playbook:` arm scopes by
|
|
662
|
-
/// siteId + leadId, and the legacy `dasset_` arm scopes by
|
|
663
|
-
/// `asset.siteId === grant.siteId`. Neither reads `workspaceId`, so a
|
|
664
|
-
/// stamped legacy grant gains no reach it did not already have.
|
|
665
|
-
/// DO NOT make the legacy or playbook arm consult this column without
|
|
666
|
-
/// re-deriving that argument — doing so WOULD be an authorization
|
|
667
|
-
/// widening, which is exactly what the pre-#6374 NULL used to prevent.
|
|
668
|
-
///
|
|
669
|
-
/// #6254 DELIBERATELY DOES NOT TOUCH THIS FIELD. It stays nullable, with
|
|
670
|
-
/// no equality CHECK — unlike Lead/ProjectListing/FlowDefinition/
|
|
671
|
-
/// FlowSession above, this column has no source column to derive from
|
|
672
|
-
/// (its own migration says so: "no source column to derive from ... a
|
|
673
|
-
/// wholly new fact, captured only at a FUTURE mint"), and `NULL` is the
|
|
674
|
-
/// serve path's fail-closed sentinel (`deliverable-asset-store.ts`
|
|
675
|
-
/// `readDeliverableAsset` opens `if (workspaceId === null) return null`).
|
|
676
|
-
/// Stamping `workspace_id := site_id` would hand a legacy grant an
|
|
677
|
-
/// authorization scope no mint ever granted, on a bearer-token download
|
|
678
|
-
/// path — a security regression, not a tightening. That reasoning still
|
|
679
|
-
/// holds for EXISTING rows, which is why the column stays nullable and why
|
|
680
|
-
/// #6374's migration adds no column and rewrites no row. Carved out of the
|
|
681
|
-
/// #6254 retrofit by the operator (spec §10.1 Q2 -> carve out); #6374
|
|
682
|
-
/// fences it at mint grain, where the workspace is actually known, and
|
|
683
|
-
/// adds the NULL-TOLERANT
|
|
684
|
-
/// deliverable_grants_workspace_site_agree CHECK — so a STAMPED grant can
|
|
685
|
-
/// never disagree with its site while a legacy grant stays storable.
|
|
686
|
-
workspaceId String? @map("workspace_id")
|
|
687
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
688
|
-
// Post-mint send-outcome stamp (#3998 D1) — set at most once, by the
|
|
689
|
-
// send-and-stamp tail shared between fulfillDeliverable and the
|
|
690
|
-
// deliverable:resend CLI. 'sent' | 'send_failed' | 'sender_unconfigured' —
|
|
691
|
-
// the exact post-mint subset of deliverable-fulfillment.ts's
|
|
692
|
-
// FulfillmentOutcome union; carried here (not a Prisma enum — this repo
|
|
693
|
-
// uses none), mirroring FlowSession.abandonEmailStatus/abandonEmailAt.
|
|
694
|
-
// NULL means the process never reached a post-mint outcome — the crash
|
|
695
|
-
// window between mint and send (#3961 D5's unhandled consequence) that
|
|
696
|
-
// this column makes queryable. This is the SELECTION term for the resend
|
|
697
|
-
// CLI's list mode only; a resend itself is an explicit operator act on one
|
|
698
|
-
// named grant and is NOT guarded by this column (#3998 D3).
|
|
699
|
-
sendStatus String? @map("send_status")
|
|
700
|
-
sendAt DateTime? @map("send_at")
|
|
701
|
-
|
|
702
|
-
// Backs the deliverable:resend CLI's list-mode query (siteId + sendStatus
|
|
703
|
-
// IS NULL) — without this, that filter set is a full table scan (the same
|
|
704
|
-
// lesson #3962's code-review finding established for FlowSession).
|
|
705
|
-
@@index([siteId, sendStatus])
|
|
706
|
-
@@map("deliverable_grants")
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/// WaitlistExperiment — one ratified experiment config per (siteId, key), the
|
|
710
|
-
/// runtime twin of a site-plan waitlistExperiments[] entry (#4661 D3/D7).
|
|
711
|
-
/// `config` is the validated experimentConfig (D1) with the three registers
|
|
712
|
-
/// EMBEDDED. Written only via upsertWaitlistExperiment (D5).
|
|
713
|
-
model WaitlistExperiment {
|
|
714
|
-
id String @id @default(dbgenerated("('wlexp_'::text || (uuidv7())::text)")) @db.Text
|
|
715
|
-
siteId String @map("site_id")
|
|
716
|
-
// #6374 — the tenancy fence, extended to this model (spec
|
|
717
|
-
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
718
|
-
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
719
|
-
// waitlist_experiments_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
720
|
-
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
721
|
-
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
722
|
-
// value, never one from a request body (AC-6).
|
|
723
|
-
workspaceId String @map("workspace_id")
|
|
724
|
-
key String
|
|
725
|
-
mode String // lead-capture | qualifying | paid
|
|
726
|
-
status String // draft | live | closed
|
|
727
|
-
config Json
|
|
728
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
729
|
-
updatedAt DateTime @updatedAt @map("updated_at")
|
|
730
|
-
|
|
731
|
-
@@unique([siteId, key])
|
|
732
|
-
@@index([workspaceId])
|
|
733
|
-
@@map("waitlist_experiments")
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/// WaitlistScore — the rules layer's output for one signup (#4661 D3; written
|
|
737
|
-
/// by A2 #4662). leadId is a plain scalar into inbox.Lead (ADR 0009). One row
|
|
738
|
-
/// per lead — re-scoring overwrites, never appends.
|
|
739
|
-
model WaitlistScore {
|
|
740
|
-
id String @id @default(dbgenerated("('wlscore_'::text || (uuidv7())::text)")) @db.Text
|
|
741
|
-
siteId String @map("site_id")
|
|
742
|
-
// #6374 — the tenancy fence, extended to this model (spec
|
|
743
|
-
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
744
|
-
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
745
|
-
// waitlist_scores_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
746
|
-
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
747
|
-
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
748
|
-
// value, never one from a request body (AC-6).
|
|
749
|
-
workspaceId String @map("workspace_id")
|
|
750
|
-
leadId String @map("lead_id")
|
|
751
|
-
experimentKey String @map("experiment_key")
|
|
752
|
-
waitlistKey String @map("waitlist_key")
|
|
753
|
-
tier String
|
|
754
|
-
score Int
|
|
755
|
-
categoryScores Json @map("category_scores")
|
|
756
|
-
ruleTrace Json @map("rule_trace")
|
|
757
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
758
|
-
updatedAt DateTime @updatedAt @map("updated_at")
|
|
759
|
-
|
|
760
|
-
// #5036 D6d — one score row per (lead, EXPERIMENT), not per lead.
|
|
761
|
-
//
|
|
762
|
-
// `@@unique([leadId])` was right for a single instrument and wrong for a
|
|
763
|
-
// marketplace of them: a visitor who joins two founders' waitlists is one
|
|
764
|
-
// lead with two scores, and the narrower constraint permitted one. The
|
|
765
|
-
// second write silently overwrote the first, so one founder's evidence
|
|
766
|
-
// disappeared and their weekly CSV under-reported with no error raised.
|
|
767
|
-
// Scoping the key to the experiment keeps the overwrite-on-retake behaviour
|
|
768
|
-
// that `scoreCompletedSignup` relies on, per project rather than globally.
|
|
769
|
-
@@unique([leadId, experimentKey])
|
|
770
|
-
@@index([siteId, experimentKey, waitlistKey])
|
|
771
|
-
@@index([workspaceId])
|
|
772
|
-
@@map("waitlist_scores")
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
/// PlaybookInstance — a compiled, person-scoped Playbook (FEAT-074 U4a,
|
|
776
|
-
/// #4715). Append-only: `scoreCompletedSignup` (A2) fires the compile on
|
|
777
|
-
/// every `targeting-maturity` completion, so a retake writes a NEW row
|
|
778
|
-
/// rather than overwriting — unlike WaitlistScore, which is
|
|
779
|
-
/// @@unique([leadId, experimentKey]) and overwritten on retake of the SAME
|
|
780
|
-
/// experiment (D2; scoped per experiment by #5036 D6d). `tier`/`ruleTrace` are SNAPSHOTS of the
|
|
781
|
-
/// WaitlistScore at compile time; `scoreId` is kept only as the provenance
|
|
782
|
-
/// pointer to the (possibly since-overwritten) live row. `source` is the
|
|
783
|
-
/// U1 `playbook-source` machine-block object; `markdown` is the rendered
|
|
784
|
-
/// `PLAYBOOK-<slug>.md` body U4b (#4718) reads to render + deliver.
|
|
785
|
-
model PlaybookInstance {
|
|
786
|
-
id String @id @default(dbgenerated("('pbk_'::text || (uuidv7())::text)")) @db.Text
|
|
787
|
-
siteId String @map("site_id")
|
|
788
|
-
// #6374 — the tenancy fence, extended to this model (spec
|
|
789
|
-
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
790
|
-
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
791
|
-
// playbook_instances_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
792
|
-
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
793
|
-
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
794
|
-
// value, never one from a request body (AC-6).
|
|
795
|
-
workspaceId String @map("workspace_id")
|
|
796
|
-
leadId String @map("lead_id")
|
|
797
|
-
experimentKey String @map("experiment_key")
|
|
798
|
-
scoreId String @map("score_id")
|
|
799
|
-
tier String
|
|
800
|
-
level Int
|
|
801
|
-
ruleTrace Json @map("rule_trace")
|
|
802
|
-
source Json
|
|
803
|
-
markdown String @db.Text
|
|
804
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
805
|
-
|
|
806
|
-
@@index([siteId, leadId, createdAt])
|
|
807
|
-
@@index([workspaceId])
|
|
808
|
-
@@map("playbook_instances")
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
/// WaitlistFollowUpSend — the audit stamp for one attempted follow-up email
|
|
812
|
-
/// (#4732 D1/D5). `@@unique([leadId, followUpKey])` IS the sweep's
|
|
813
|
-
/// idempotency guarantee (mirrors WaitlistScore's own `@@unique([leadId])`
|
|
814
|
-
/// shape) — the stateless `runWaitlistFollowUpSweep` (#4732)
|
|
815
|
-
/// selects candidates by the ABSENCE of a matching row, so a re-run never
|
|
816
|
-
/// double-sends. `status` is 'sent' | 'failed' (no Prisma enum — this repo
|
|
817
|
-
/// uses none, mirroring DeliverableGrant.sendStatus's free-string
|
|
818
|
-
/// convention); 'failed' is terminal, no auto-retry (the abandon-email
|
|
819
|
-
/// sweep's own posture) — a suppressed/skipped candidate (wrong tier, not
|
|
820
|
-
/// yet due, a booked call, or unsubscribed) is NEVER stamped here at all, so
|
|
821
|
-
/// it is re-evaluated on every later sweep run until it either sends or its
|
|
822
|
-
/// completedAt/offsetDays window is permanently past.
|
|
823
|
-
model WaitlistFollowUpSend {
|
|
824
|
-
id String @id @default(dbgenerated("('wlfu_'::text || (uuidv7())::text)")) @db.Text
|
|
825
|
-
siteId String @map("site_id")
|
|
826
|
-
// #6374 — the tenancy fence, extended to this model (spec
|
|
827
|
-
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
828
|
-
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
829
|
-
// waitlist_followup_sends_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
830
|
-
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
831
|
-
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
832
|
-
// value, never one from a request body (AC-6).
|
|
833
|
-
workspaceId String @map("workspace_id")
|
|
834
|
-
leadId String @map("lead_id")
|
|
835
|
-
followUpKey String @map("follow_up_key")
|
|
836
|
-
sentAt DateTime @map("sent_at")
|
|
837
|
-
status String
|
|
838
|
-
|
|
839
|
-
@@unique([leadId, followUpKey])
|
|
840
|
-
@@index([siteId])
|
|
841
|
-
@@index([workspaceId])
|
|
842
|
-
@@map("waitlist_followup_sends")
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
/// ListingCsvSend — the weekly founder-CSV send stamp (#5001). One row per
|
|
846
|
-
/// DELIVERED CSV for one ProjectListing: `sentAt` is the watermark the next
|
|
847
|
-
/// sweep selects after (signups whose WaitlistScore landed at or before it are
|
|
848
|
-
/// already in a delivered CSV), `rowCount` is how many rows that CSV carried,
|
|
849
|
-
/// and `grantId` is the DeliverableGrant whose id is the capability token in
|
|
850
|
-
/// the founder's download link. Plain-scalar refs to ProjectListing /
|
|
851
|
-
/// DeliverableGrant (ADR 0009 invariant 1) — no cross-model @relation. A
|
|
852
|
-
/// listing with no new signups gets no row, so "no new rows ⇒ no send" is
|
|
853
|
-
/// visible in the data, not only in the sweep's control flow.
|
|
854
|
-
model ListingCsvSend {
|
|
855
|
-
id String @id @default(dbgenerated("('lcsv_'::text || (uuidv7())::text)")) @db.Text
|
|
856
|
-
// #6374 D2 — the tenancy fence, in the shape this model's ABSENT site_id
|
|
857
|
-
// forces (operator disposition, 2026-09-20). It gets NOT NULL plus the
|
|
858
|
-
// listing_csv_sends_workspace_id_format CHECK and NOTHING else: with no
|
|
859
|
-
// site_id column there is nothing for an equality CHECK to compare, and a
|
|
860
|
-
// composite FK to ProjectListing — legal, both models are inbox — was
|
|
861
|
-
// offered and declined in favour of ONE fence shape. Backfilled and
|
|
862
|
-
// stamped from the parent listing row, never from a request body (AC-6).
|
|
863
|
-
workspaceId String @map("workspace_id")
|
|
864
|
-
listingId String @map("listing_id")
|
|
865
|
-
sentAt DateTime @map("sent_at")
|
|
866
|
-
rowCount Int @map("row_count")
|
|
867
|
-
grantId String @map("grant_id")
|
|
868
|
-
|
|
869
|
-
@@index([listingId, sentAt])
|
|
870
|
-
@@index([workspaceId])
|
|
871
|
-
@@map("listing_csv_sends")
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
/// FlowCheckout — the recommender buyer record (#4837 D2/D4): one row per
|
|
875
|
-
/// checkout ATTEMPT. `amountMinor` is the server's own `price().total` × 100
|
|
876
|
-
/// (D6); `quote` is the full WaitlistPriceQuote beside it, so an onboarding
|
|
877
|
-
/// reader sees the seat line and credit line, not only a total. `status` is
|
|
878
|
-
/// 'created' | 'completed' | 'expired' (no Prisma enum — this repo uses none,
|
|
879
|
-
/// mirroring DeliverableGrant.sendStatus's free-string convention); the
|
|
880
|
-
/// webhook (#4838) advances it with a guarded update on 'created'. Plain-scalar
|
|
881
|
-
/// refs to Lead / FlowSession (ADR 0009 invariant 1) — no cross-model
|
|
882
|
-
/// @relation. `permalink` is the answers-only payload string the visitor's
|
|
883
|
-
/// recommendation was rendered from (#4770). Written only by
|
|
884
|
-
/// apps/api/src/services/recommender-capture.ts.
|
|
885
|
-
model FlowCheckout {
|
|
886
|
-
id String @id @default(dbgenerated("('flchk_'::text || (uuidv7())::text)")) @db.Text
|
|
887
|
-
siteId String @map("site_id")
|
|
888
|
-
// #6374 — the tenancy fence, extended to this model (spec
|
|
889
|
-
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
890
|
-
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
891
|
-
// flow_checkouts_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
892
|
-
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
893
|
-
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
894
|
-
// value, never one from a request body (AC-6).
|
|
895
|
-
workspaceId String @map("workspace_id")
|
|
896
|
-
flowSlug String @map("flow_slug")
|
|
897
|
-
experimentKey String @map("experiment_key")
|
|
898
|
-
sessionId String @map("session_id")
|
|
899
|
-
leadId String @map("lead_id")
|
|
900
|
-
offerKey String @map("offer_key")
|
|
901
|
-
seats Int
|
|
902
|
-
period String
|
|
903
|
-
creditStop Json @map("credit_stop")
|
|
904
|
-
amountMinor Int @map("amount_minor")
|
|
905
|
-
currency String
|
|
906
|
-
quote Json
|
|
907
|
-
permalink String @db.Text
|
|
908
|
-
stripeSessionId String? @map("stripe_session_id")
|
|
909
|
-
// The hosted Checkout page URL an identical resubmission is replayed with
|
|
910
|
-
// (#4912 D6) — stamped in the same write as `stripeSessionId`, because a
|
|
911
|
-
// Checkout Session URL cannot be reconstructed from its id.
|
|
912
|
-
checkoutUrl String? @db.Text @map("checkout_url")
|
|
913
|
-
// The server-derived fingerprint of the checkout request (#4912 D5): a
|
|
914
|
-
// SHA-256 hex digest, never the request fields themselves. Null on
|
|
915
|
-
// pre-#4912 rows and on a row whose key a reconcile released.
|
|
916
|
-
idempotencyKey String? @map("idempotency_key")
|
|
917
|
-
stripeCustomerId String? @map("stripe_customer_id")
|
|
918
|
-
status String
|
|
919
|
-
completedAt DateTime? @map("completed_at")
|
|
920
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
921
|
-
updatedAt DateTime @updatedAt @map("updated_at")
|
|
922
|
-
|
|
923
|
-
// The webhook's join (#4838) — nullable, so `created` rows with no session
|
|
924
|
-
// id yet never collide.
|
|
925
|
-
@@unique([stripeSessionId])
|
|
926
|
-
// The /checkout idempotency guarantee (#4912 D5), tenant-scoped so a
|
|
927
|
-
// fingerprint can never reach another site's row. Nullable, so pre-#4912
|
|
928
|
-
// rows and reconciled rows never collide.
|
|
929
|
-
@@unique([siteId, idempotencyKey])
|
|
930
|
-
// The dashboard's checkedOut read (#4839 D3): siteId + experimentKey + status.
|
|
931
|
-
@@index([siteId, experimentKey, status])
|
|
932
|
-
// Onboarding's read: everything a lead bought.
|
|
933
|
-
@@index([leadId])
|
|
934
|
-
@@index([workspaceId])
|
|
935
|
-
@@map("flow_checkouts")
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
/// ProjectListing — the founder's application row for the project-listings
|
|
939
|
-
/// workflow (#4998): apply → Slack approve/reject → build issue → data-driven
|
|
940
|
-
/// page + diagnostic → weekly CSV. `leadId` is a plain scalar ref into this
|
|
941
|
-
/// namespace's own `Lead` (ADR 0009 invariant 1) — the POST route writes both
|
|
942
|
-
/// rows via `routeLead` (kind `submit`, source `projects-apply`) then this
|
|
943
|
-
/// row, never the reverse. `questions` is the diagnostic template instance
|
|
944
|
-
/// (Q2–Q7 + up to 3 extras, bounded by `projectListingQuestionsSchema` in
|
|
945
|
-
/// `@working-theory/validation`) per
|
|
946
|
-
/// docs/decisions/2026-09-07-project-listings-workflow.md (consumer repo,
|
|
947
|
-
/// salessmyth/samhenry — this row and its routes vendor down, operator
|
|
948
|
-
/// 2026-09-07 option 2). `status` is a free string (`applied` | `approved` |
|
|
949
|
-
/// `rejected` | `active`, no Prisma enum — this repo uses none, mirroring
|
|
950
|
-
/// `FlowCheckout.status`'s convention). Those are the four STORED values
|
|
951
|
-
/// (#5033): the fifth lifecycle state, `closed`, is COMPUTED in the read
|
|
952
|
-
/// from `windowEndsAt` and never written, so an expired listing cannot stay
|
|
953
|
-
/// publicly open because a scheduled job failed. `draft` is defined so the
|
|
954
|
-
/// vocabulary never migrates again and is reachable through no route today.
|
|
955
|
-
/// `decidedAt`/`decidedBy` stamp the approve/reject moment, and an approve
|
|
956
|
-
/// also stamps `windowEndsAt = decidedAt + windowDays`. `windowDays` is the
|
|
957
|
-
/// applicant's requested window ALREADY CLAMPED at create against the site's
|
|
958
|
-
/// `SiteListingConfig` (database/config/schema.prisma) — the config itself
|
|
959
|
-
/// is never accepted from a request body. `issueUrl` (the build issue,
|
|
960
|
-
/// #5000) and `experimentId` (the generated flow's waitlist experiment,
|
|
961
|
-
/// #5001) are null until their respective downstream unit lands.
|
|
962
|
-
/// `featured` is the operator-set shelf flag, surfaced on the public read
|
|
963
|
-
/// only where the site's config enables it. `headline`, `logoMarkUrl` and
|
|
964
|
-
/// `logoWordmarkUrl` are the listing card's presentation fields; the two
|
|
965
|
-
/// logo columns ship nullable with NO upload path (#5033 D5 — no storage
|
|
966
|
-
/// dependency exists, and accepting arbitrary files from anonymous
|
|
967
|
-
/// strangers deserves its own review), and a card with both absent renders
|
|
968
|
-
/// a designed name-only treatment. `slackChannel`/`slackTs`
|
|
969
|
-
/// (#4999) are the posted application message's coordinates, stamped by
|
|
970
|
-
/// `postListingApplication` right after `chat.postMessage` succeeds, so the
|
|
971
|
-
/// decision handler can `chat.update` the same message in place; null when
|
|
972
|
-
/// Slack is unconfigured or the post failed. Still out of scope here: the
|
|
973
|
-
/// GitHub issue (#5000), the diagnostic flow + weekly CSV (#5001).
|
|
974
|
-
model ProjectListing {
|
|
975
|
-
id String @id @default(dbgenerated("('prjl_'::text || (uuidv7())::text)")) @db.Text
|
|
976
|
-
siteId String @map("site_id")
|
|
977
|
-
// #6203 — the isolation key `tenantDb(tenant).projectListings` filters on.
|
|
978
|
-
// Plain scalar, no @relation (ADR 0009 D1, same reasoning as ownerUserId
|
|
979
|
-
// below).
|
|
980
|
-
//
|
|
981
|
-
// #6254 AC-3/AC-4/AC-7 — NOT NULL as of
|
|
982
|
-
// database/migrations/20260922120000_tenancy_fence, which also normalizes
|
|
983
|
-
// `siteId` to the resolved workspace id and adds
|
|
984
|
-
// `project_listings_workspace_site_agree CHECK ("workspace_id" =
|
|
985
|
-
// "site_id")` — see `Lead.workspaceId` (top of database/inbox/schema.prisma)
|
|
986
|
-
// for the full rationale. Every write site derives this value
|
|
987
|
-
// server-side; never from a request body (AC-6).
|
|
988
|
-
workspaceId String @map("workspace_id")
|
|
989
|
-
// #5036 — written at SUBMIT, not at start. The founder's contact details are
|
|
990
|
-
// already captured on THIS row at step one, so a `Lead` at step one adds no
|
|
991
|
-
// capture; what it does add is an operator notification email per abandoned
|
|
992
|
-
// first step, on an anonymous route (PR #5042 security review, finding 4).
|
|
993
|
-
// `routeLead` remains the one Lead write path; it just runs later.
|
|
994
|
-
leadId String? @map("lead_id")
|
|
995
|
-
// #5036 D1 — every field the SUBMIT payload requires is nullable at rest.
|
|
996
|
-
// Validation moved from write-time to submit-time when the application
|
|
997
|
-
// became a ten-step draft that saves as it goes; a half-filled draft is a
|
|
998
|
-
// legitimate row, and a NOT NULL here would make one unstorable. Only
|
|
999
|
-
// `submit` asserts completeness.
|
|
1000
|
-
slug String?
|
|
1001
|
-
name String?
|
|
1002
|
-
oneLiner String? @map("one_liner")
|
|
1003
|
-
problem String? @db.Text
|
|
1004
|
-
// #5096 — `problem`'s narrative other half ("how are you solving it?").
|
|
1005
|
-
// OPT-IN, not required: it is absent from the platform's default
|
|
1006
|
-
// `collectedFields` mask, so a site collects it only by naming it.
|
|
1007
|
-
solution String? @db.Text
|
|
1008
|
-
url String?
|
|
1009
|
-
// #5036 D2 — the six-rung product ladder that replaced the four-value
|
|
1010
|
-
// `stage`. The migration remaps every old value by an explicit map; there
|
|
1011
|
-
// is no shim, because the two ladders do not align rung-for-rung.
|
|
1012
|
-
product String?
|
|
1013
|
-
// #5036 D3/D5 — the two segmentation axes shared with the diagnostic.
|
|
1014
|
-
commitment String?
|
|
1015
|
-
teamComposition String? @map("team_composition")
|
|
1016
|
-
// #5036 D5 — exactly three {question, answer} entries, the detail page's
|
|
1017
|
-
// fixed FAQ slot.
|
|
1018
|
-
faqs Json?
|
|
1019
|
-
anythingElse String? @db.Text @map("anything_else")
|
|
1020
|
-
// #5036 D6 — a legal control, not a contact detail: visitors answering this
|
|
1021
|
-
// founder's diagnostic are owed the identity and address of the party
|
|
1022
|
-
// collecting their data. Carried on the PUBLIC read for that reason.
|
|
1023
|
-
//
|
|
1024
|
-
// #5279 — still the single-line DISPLAY string. Where the six part columns
|
|
1025
|
-
// below are written, this is COMPOSED from them server-side; where they are
|
|
1026
|
-
// not, it is the line the founder typed, verbatim.
|
|
1027
|
-
address String?
|
|
1028
|
-
// #5279 — the structured parts of the address above, beside it rather than
|
|
1029
|
-
// instead of it. Nullable for the same reason as the #5036 D1 block above: a
|
|
1030
|
-
// row written before #5279 has no parts, and none may ever be GUESSED from
|
|
1031
|
-
// its `address` string. Parts compose into a line deterministically; a line
|
|
1032
|
-
// does not decompose into parts, and a wrong split silently corrupts the
|
|
1033
|
-
// party identification the listing's privacy notice points at — so the
|
|
1034
|
-
// migration deliberately backfills nothing and a null part stays null for
|
|
1035
|
-
// the life of the row. Composition runs one way only, in `draftUpdateData`
|
|
1036
|
-
// (apps/api/src/services/project-listings.ts).
|
|
1037
|
-
addressLine1 String? @map("address_line1")
|
|
1038
|
-
addressLine2 String? @map("address_line2")
|
|
1039
|
-
addressCity String? @map("address_city")
|
|
1040
|
-
addressRegion String? @map("address_region")
|
|
1041
|
-
addressPostalCode String? @map("address_postal_code")
|
|
1042
|
-
addressCountry String? @map("address_country")
|
|
1043
|
-
questions Json?
|
|
1044
|
-
// #5285 — the answers to the site's OWN questions: one flat JSON object of
|
|
1045
|
-
// `{ questionId: answer }` for everything a site asks that the PLATFORM does
|
|
1046
|
-
// not read. The field contract (packages/validation/.../listing-
|
|
1047
|
-
// application-contract.ts) names what the platform reads and therefore what
|
|
1048
|
-
// earns a column; everything else was previously unaskable without a
|
|
1049
|
-
// migration and a release, which is what this column removes.
|
|
1050
|
-
//
|
|
1051
|
-
// BOUNDED, not free-form, and that is the point: the write path is
|
|
1052
|
-
// anonymous (a resume token and nothing else), so an unbounded JSON bag on
|
|
1053
|
-
// it is a place for a stranger to park arbitrary text in the table. The
|
|
1054
|
-
// server accepts only ids the SITE declared in its own `SiteListingConfig`,
|
|
1055
|
-
// at that site's own count and length caps, and refuses the whole request
|
|
1056
|
-
// otherwise — so what lands here is never larger than the questions the
|
|
1057
|
-
// operator chose to ask. Null on every row written before #5285 and on
|
|
1058
|
-
// every site that asks nothing of its own.
|
|
1059
|
-
//
|
|
1060
|
-
// Only the `public`-visibility subset rides the public read; an
|
|
1061
|
-
// `operator-only` answer reaches the dashboard and the review payload and
|
|
1062
|
-
// stops there.
|
|
1063
|
-
siteAnswers Json? @map("site_answers")
|
|
1064
|
-
// #5407 — still the single-line DISPLAY name, and still NOT NULL: `start`
|
|
1065
|
-
// has always required one and every reader below expects one. Where the two
|
|
1066
|
-
// part columns beside it are written, this is COMPOSED from them
|
|
1067
|
-
// server-side (`startProjectListingDraft`); where they are not, it is the
|
|
1068
|
-
// line the founder typed, verbatim.
|
|
1069
|
-
//
|
|
1070
|
-
// Read by: the submit-time slug fallback, the approve/reject email's
|
|
1071
|
-
// greeting, the Slack review card's Founder row, the build issue, and the
|
|
1072
|
-
// `routeLead` identity. None of them changes.
|
|
1073
|
-
founderName String @map("founder_name")
|
|
1074
|
-
// #5407 — the structured parts of the display name above, beside it rather
|
|
1075
|
-
// than instead of it, exactly as #5279 put the six address parts beside
|
|
1076
|
-
// `address`. Nullable for the #5036 D1 reason and one more: a row written
|
|
1077
|
-
// before #5407 has no parts, and none may ever be GUESSED from its
|
|
1078
|
-
// `founder_name`. "First Last" composes one way only; a name does not
|
|
1079
|
-
// decompose (a two-word split mangles "van der Berg", "Maria del Carmen"
|
|
1080
|
-
// and every mononym), so the migration backfills nothing and a null part
|
|
1081
|
-
// stays null for the life of the row.
|
|
1082
|
-
founderFirstName String? @map("founder_first_name")
|
|
1083
|
-
founderLastName String? @map("founder_last_name")
|
|
1084
|
-
founderEmail String @map("founder_email")
|
|
1085
|
-
// #5407 — the founder's phone, collected on the default contact screen
|
|
1086
|
-
// alongside the email (samhenry spec 316 D1). Nullable at rest for the
|
|
1087
|
-
// #5036 D1 reason AND permanently: a row written before #5407 has none, and
|
|
1088
|
-
// a site may declare the question optional through its own config.
|
|
1089
|
-
//
|
|
1090
|
-
// NEVER on the public read: `projectListingPublicSchema` omits it beside
|
|
1091
|
-
// `founderEmail`. The address on this row is a legal control a visitor is
|
|
1092
|
-
// owed; a phone number is a contact detail nobody outside the operator is.
|
|
1093
|
-
founderPhone String? @map("founder_phone")
|
|
1094
|
-
// #5407 — sha-256 of the EMAIL VERIFICATION NONCE, never the nonce itself.
|
|
1095
|
-
//
|
|
1096
|
-
// A second credential beside `resumeTokenHash`, and the reason
|
|
1097
|
-
// `emailVerifiedAt` below means what its name says. The resume token is
|
|
1098
|
-
// returned to WHOEVER POSTED `start`'s FORM, so presenting it proves only
|
|
1099
|
-
// that you opened the draft. This nonce is minted at `start`, digested
|
|
1100
|
-
// here, and handed to nothing except the resume-email builder
|
|
1101
|
-
// (`services/project-listing-resume-email.ts`): it is never in a response
|
|
1102
|
-
// body, never in a log, and never in the browser that filled the form. The
|
|
1103
|
-
// only way to hold it is to have read mail sent to `founder_email`.
|
|
1104
|
-
//
|
|
1105
|
-
// Null on every row written before #5407, and on every row whose site has
|
|
1106
|
-
// not switched the resume email on — there the nonce exists and is simply
|
|
1107
|
-
// never delivered, so nothing can verify, which is the correct outcome.
|
|
1108
|
-
// Cleared at `submit` beside `resumeTokenHash`, for the same reason: a
|
|
1109
|
-
// credential that leaks after the draft stops being a draft is inert twice
|
|
1110
|
-
// over.
|
|
1111
|
-
emailVerifyNonceHash String? @unique @map("email_verify_nonce_hash")
|
|
1112
|
-
// #5407 — when the founder proved control of `founder_email`, by opening
|
|
1113
|
-
// the link mailed to it.
|
|
1114
|
-
//
|
|
1115
|
-
// WRITE-ONCE. `verifyProjectListingDraftEmail` sets it only where it is
|
|
1116
|
-
// still null, so a founder who opens the link five times is stamped with
|
|
1117
|
-
// the FIRST opening rather than the latest.
|
|
1118
|
-
//
|
|
1119
|
-
// WHAT IT PROVES, and its exact limit. The stamp requires the nonce above,
|
|
1120
|
-
// which left this server only inside the email. So it says the holder can
|
|
1121
|
-
// read mail sent to `founder_email` — which is the whole of what "email
|
|
1122
|
-
// verified" claims. It says nothing about WHO they are: `owner_user_id`
|
|
1123
|
-
// (#5295) is the column that carries an identity claim, and it still needs
|
|
1124
|
-
// a signed-in user whose own address the auth system verified. A row can
|
|
1125
|
-
// carry either, both, or neither.
|
|
1126
|
-
//
|
|
1127
|
-
// Permanently nullable, and NOT a backfill target. Every row written before
|
|
1128
|
-
// #5407 predates the stamp; asserting verification on one would assert
|
|
1129
|
-
// exactly the thing nobody proved. Null means "not verified", never
|
|
1130
|
-
// "verification failed", and it is also what a site running no resume email
|
|
1131
|
-
// reads forever.
|
|
1132
|
-
emailVerifiedAt DateTime? @map("email_verified_at")
|
|
1133
|
-
founderCompany String? @map("founder_company")
|
|
1134
|
-
status String
|
|
1135
|
-
// #5036 D1/D6 — when the founder ticked step one's consent box. Never null:
|
|
1136
|
-
// a row cannot exist without it, because `start` requires it.
|
|
1137
|
-
consentAt DateTime @default(now()) @map("consent_at") @db.Timestamptz(3)
|
|
1138
|
-
// #5036 D1 — sha-256 of the opaque resume token, never the token itself.
|
|
1139
|
-
// The token is the ONLY credential on an anonymous surface, so the column
|
|
1140
|
-
// holds a digest an attacker who reads the table still cannot present.
|
|
1141
|
-
// Cleared at submit, which makes a leaked token inert the moment the draft
|
|
1142
|
-
// stops being a draft.
|
|
1143
|
-
resumeTokenHash String? @unique @map("resume_token_hash")
|
|
1144
|
-
headline String?
|
|
1145
|
-
logoMarkUrl String? @map("logo_mark_url")
|
|
1146
|
-
logoWordmarkUrl String? @map("logo_wordmark_url")
|
|
1147
|
-
featured Boolean @default(false)
|
|
1148
|
-
decidedAt DateTime? @map("decided_at")
|
|
1149
|
-
decidedBy String? @map("decided_by")
|
|
1150
|
-
// The operator's copy-edit audit trio. A listing that is `requested` or
|
|
1151
|
-
// `active` can have its PUBLISHED COPY trimmed by the operator (headline,
|
|
1152
|
-
// oneLiner, problem, faqs, questions) — the third review outcome beside
|
|
1153
|
-
// approve and reject. `copyEditedAt`/`copyEditedBy` mirror the decision pair
|
|
1154
|
-
// directly above and are re-stamped on every edit.
|
|
1155
|
-
//
|
|
1156
|
-
// `submittedCopy` is WRITE-ONCE, on the FIRST edit only: it holds those five
|
|
1157
|
-
// fields exactly as the applicant submitted them. Null for the untouched
|
|
1158
|
-
// majority, which is why "what did the applicant actually send" reads as the
|
|
1159
|
-
// snapshot if present and the row itself otherwise. Never cleared, so the
|
|
1160
|
-
// submitted copy stays recoverable for the life of the row.
|
|
1161
|
-
submittedCopy Json? @map("submitted_copy")
|
|
1162
|
-
copyEditedAt DateTime? @map("copy_edited_at")
|
|
1163
|
-
copyEditedBy String? @map("copy_edited_by")
|
|
1164
|
-
issueUrl String? @map("issue_url")
|
|
1165
|
-
// #5036 D6d — the experiment's KEY, not its id. `WaitlistScore` holds
|
|
1166
|
-
// `experimentKey`, so holding the id here meant every listing-to-submissions
|
|
1167
|
-
// read fetched the experiment purely to learn its key. One hop, the same
|
|
1168
|
-
// string on both sides. ADR 0009 forbids a relation across the namespace
|
|
1169
|
-
// boundary, so this is a plain string matched by convention — which is
|
|
1170
|
-
// exactly why it has to be the string the other side actually stores.
|
|
1171
|
-
experimentKey String? @map("experiment_key")
|
|
1172
|
-
windowDays Int? @map("window_days")
|
|
1173
|
-
windowEndsAt DateTime? @map("window_ends_at")
|
|
1174
|
-
// The posted application message's coordinates (#4999) — set by
|
|
1175
|
-
// `postListingApplication` right after `chat.postMessage` succeeds, so the
|
|
1176
|
-
// Approve/Reject decision handler can `chat.update` the SAME message in
|
|
1177
|
-
// place. Null when Slack is unconfigured or the post failed (create never
|
|
1178
|
-
// rolls back on a Slack failure).
|
|
1179
|
-
slackChannel String? @map("slack_channel")
|
|
1180
|
-
slackTs String? @map("slack_ts")
|
|
1181
|
-
// Listing -> owner-user link (#5295). Plain scalars — no cross-namespace
|
|
1182
|
-
// @relation into auth.User (ADR 0009 invariant #1), exactly as
|
|
1183
|
-
// `Lead.promotedUserId` / `promotedAt` above. Set by
|
|
1184
|
-
// apps/api/src/services/listing-promotion.ts once the reconciling user's own
|
|
1185
|
-
// email is VERIFIED, that verified address matches `founderEmail`, and this
|
|
1186
|
-
// row's `siteId` resolves to one of that user's own workspaces.
|
|
1187
|
-
//
|
|
1188
|
-
// `founderEmail` is an UNVERIFIED string typed into an anonymous form, so it
|
|
1189
|
-
// can never by itself say who owns the row: two people who type the same
|
|
1190
|
-
// address are two people. This column is the only ownership claim any read
|
|
1191
|
-
// may join on, and nothing derives it from `founderEmail` at read time.
|
|
1192
|
-
//
|
|
1193
|
-
// Nullable PERMANENTLY, not pending a backfill. A listing submitted by
|
|
1194
|
-
// someone who never signs in has no owner for the life of the row, and that
|
|
1195
|
-
// is a legitimate resting state: a backfill from `founderEmail` would assert
|
|
1196
|
-
// a link nobody proved, which is the bug this column exists to remove.
|
|
1197
|
-
ownerUserId String? @map("owner_user_id")
|
|
1198
|
-
ownerLinkedAt DateTime? @map("owner_linked_at")
|
|
1199
|
-
// #6314 — the listicle-kind `FlowDefinition` that CAPTURED this
|
|
1200
|
-
// application: the magnet the visitor arrived through, so a magnet's own
|
|
1201
|
-
// Applications list can be scoped to its own submissions rather than to the
|
|
1202
|
-
// whole workspace (nothing constrains a workspace to one listicle magnet —
|
|
1203
|
-
// `FlowDefinition` carries `@@unique([siteId, slug])` and no
|
|
1204
|
-
// `@@unique([siteId, kind])`, so workspace-only scoping would conflate two).
|
|
1205
|
-
//
|
|
1206
|
-
// NOT `FlowDefinition.listingId` reversed: that column is written at
|
|
1207
|
-
// APPROVAL time by `services/project-listing-flow.ts`, when an approved
|
|
1208
|
-
// listing is projected into its own per-listing diagnostic sub-flow. A
|
|
1209
|
-
// different link, at a different time, in the other direction.
|
|
1210
|
-
//
|
|
1211
|
-
// AN ID, NOT A SLUG (operator ruling 2026-09-19): a slug is a natural key
|
|
1212
|
-
// `@@unique([siteId, slug])` implies is mutable, and `Lead.source`'s
|
|
1213
|
-
// `'flow:' + slug` pattern is the weaker one, used there only because that
|
|
1214
|
-
// write path has no resolve step. This one does — the wire carries a slug
|
|
1215
|
-
// (no public surface hands a visitor a `flow_<uuidv7>`), and
|
|
1216
|
-
// `startProjectListingDraft` resolves it server-side against `siteId` +
|
|
1217
|
-
// `workspaceId` + `kind: "listicle"` before storing the id.
|
|
1218
|
-
//
|
|
1219
|
-
// PLAIN SCALAR REF — no `@relation`, no foreign key, no `onDelete`
|
|
1220
|
-
// (operator decision 2026-09-20, withdrawing the ruling's "on-delete
|
|
1221
|
-
// set-null" clause). Both models live in `inbox/`, so ADR 0009 invariant #1
|
|
1222
|
-
// (which bans CROSS-namespace relations) does not reach this column and
|
|
1223
|
-
// permission was never the question — the convention is: every
|
|
1224
|
-
// intra-`inbox` reference on this model and its siblings (`leadId`,
|
|
1225
|
-
// `listingId`, `grantId`, `flowSessionId`) is a plain scalar, and of the 24
|
|
1226
|
-
// FOREIGN KEY constraints in `database/migrations/**` every one targets
|
|
1227
|
-
// `auth`. The composite shape specifically was offered and declined on
|
|
1228
|
-
// `ListingCsvSend` below.
|
|
1229
|
-
//
|
|
1230
|
-
// WHAT THAT GIVES UP, stated rather than argued away: the database does not
|
|
1231
|
-
// constrain this to name a real magnet, nor one in the same workspace. Three
|
|
1232
|
-
// things bound it — the resolve carries `siteId` and `kind`, so storing a
|
|
1233
|
-
// foreign id needs the service layer to actively misbehave; nothing deletes
|
|
1234
|
-
// a `FlowDefinition` (`routes/magnets/index.ts` D5: "No DELETE"), so
|
|
1235
|
-
// `SET NULL` would have described no live code path; and the read carries
|
|
1236
|
-
// the injected `workspaceId` alongside this id, so a dangling or foreign
|
|
1237
|
-
// value yields NO rows, never another workspace's.
|
|
1238
|
-
//
|
|
1239
|
-
// WRITE-ONCE, at START. The magnet a visitor arrived through is knowable
|
|
1240
|
-
// only on arrival — by submit, ten steps and possibly a resume-email
|
|
1241
|
-
// round-trip later, the referring context is gone. `draftUpdateData` never
|
|
1242
|
-
// learns this key, so no patch body can re-attribute a draft after the fact,
|
|
1243
|
-
// the same shape `emailVerifiedAt`'s write-once stamp has.
|
|
1244
|
-
//
|
|
1245
|
-
// PERMANENTLY NULLABLE, and NOT a backfill target. Every row written before
|
|
1246
|
-
// this column has no evidence naming the magnet that captured it — no
|
|
1247
|
-
// referrer, no session link, no slug — so a backfill would have to guess
|
|
1248
|
-
// "the workspace's only listicle magnet", which is exactly the conflation
|
|
1249
|
-
// this column exists to remove, made permanent and unauditable. It is the
|
|
1250
|
-
// reason `founder_name` parts (#5407), the address parts (#5279) and
|
|
1251
|
-
// `owner_user_id` (#5295) each backfill nothing: a value that can only be
|
|
1252
|
-
// guessed must not be asserted. NULL means "not attributed" — never
|
|
1253
|
-
// "attribution failed" and never "captured directly": an application
|
|
1254
|
-
// started through a direct link AFTER this ships is NULL for the same
|
|
1255
|
-
// reason and is indistinguishable from a legacy row.
|
|
1256
|
-
capturedByFlowId String? @map("captured_by_flow_id")
|
|
1257
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
1258
|
-
updatedAt DateTime @updatedAt @map("updated_at")
|
|
1259
|
-
|
|
1260
|
-
@@unique([siteId, slug])
|
|
1261
|
-
@@index([siteId, status])
|
|
1262
|
-
// #5036 D7 — the concurrent-listing limit counts a founder's non-terminal
|
|
1263
|
-
// rows on every submit, from an anonymous route. Indexed so a stranger
|
|
1264
|
-
// cannot make that count a table scan.
|
|
1265
|
-
@@index([siteId, founderEmail])
|
|
1266
|
-
// The self-scoped read's whole WHERE clause (#5295) is `{ ownerUserId }` —
|
|
1267
|
-
// the owner link IS the scope, so no site term joins it. Single-column by
|
|
1268
|
-
// design: the promotion match (`founderEmail` + `siteId` + null owner) is
|
|
1269
|
-
// already served by the (site_id, founder_email) index above.
|
|
1270
|
-
@@index([ownerUserId])
|
|
1271
|
-
// #6203 — backs a per-workspace list (tenantDb(tenant).projectListings.list).
|
|
1272
|
-
@@index([workspaceId])
|
|
1273
|
-
// #6314 — backs the per-magnet Applications read
|
|
1274
|
-
// (tenantDb(tenant).projectListings.listByCapturingFlow), whose WHERE is
|
|
1275
|
-
// exactly these two columns. Leading on `workspaceId`, matching the
|
|
1276
|
-
// `@@index([workspaceId])` convention the fence set on this table. ONE
|
|
1277
|
-
// index: no read reaches `capturedByFlowId` without a tenant, and with no
|
|
1278
|
-
// foreign key there is no cascade wanting a referencing-side index either.
|
|
1279
|
-
@@index([workspaceId, capturedByFlowId])
|
|
1280
|
-
@@map("project_listings")
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
/// ConsentGrant — one person's answer to one consent purpose, with the exact
|
|
1284
|
-
/// wording they read (#5275). APPEND-ONLY: current state is the newest row
|
|
1285
|
-
/// per (subject, purpose), so a later withdrawal never destroys the evidence
|
|
1286
|
-
/// of the earlier grant (D9) — no `updatedAt`, and deliberately NO unique
|
|
1287
|
-
/// constraint on subject + purpose. DECLINES ARE STORED as `granted: false`
|
|
1288
|
-
/// (D8): an absent row cannot distinguish "they said no" from "we never
|
|
1289
|
-
/// asked", and on an optional purpose that distinction is the only thing the
|
|
1290
|
-
/// record is for. The subject is a type + id pair of plain scalars
|
|
1291
|
-
/// (`project_listing` / `subscriber` / …) with NO cross-namespace @relation
|
|
1292
|
-
/// (D10, ADR 0009 invariant 1) — which is what lets a new surface adopt
|
|
1293
|
-
/// consent with a service call and no migration. `statement` is the resolved
|
|
1294
|
-
/// text DENORMALIZED at write time (D7): a pointer into the site's config row
|
|
1295
|
-
/// would prove nothing once that row is edited, and `statementHash` groups
|
|
1296
|
-
/// identical wordings. `locale` is the locale the statement was actually
|
|
1297
|
-
/// served in, not the one requested. `purposeId` is the site-config registry
|
|
1298
|
-
/// id — the join key across surfaces. Two writers: apps/api/src/services/consent.ts
|
|
1299
|
-
/// (every source), and the web sign-up hook apps/web/lib/sign-up-agreements.ts
|
|
1300
|
-
/// (#8765 D4), which writes only `sign_up` rows (subject `user`) of a user it
|
|
1301
|
-
/// creates in the same Better Auth transaction, from evidence the API built
|
|
1302
|
-
/// with consent.ts's own functions.
|
|
1303
|
-
model ConsentGrant {
|
|
1304
|
-
id String @id @default(dbgenerated("('cnsnt_'::text || (uuidv7())::text)")) @db.Text
|
|
1305
|
-
siteId String @map("site_id")
|
|
1306
|
-
// #6374 — the tenancy fence, extended to this model (spec
|
|
1307
|
-
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
1308
|
-
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
1309
|
-
// consent_grants_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
1310
|
-
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
1311
|
-
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
1312
|
-
// value, never one from a request body (AC-6).
|
|
1313
|
-
workspaceId String @map("workspace_id")
|
|
1314
|
-
subjectType String @map("subject_type")
|
|
1315
|
-
subjectId String @map("subject_id")
|
|
1316
|
-
purposeId String @map("purpose_id")
|
|
1317
|
-
granted Boolean
|
|
1318
|
-
locale String
|
|
1319
|
-
statement String @db.Text
|
|
1320
|
-
statementHash String @map("statement_hash")
|
|
1321
|
-
// #8438 D2 — the Person this row counts toward. A plain scalar with no
|
|
1322
|
-
// foreign key (this table's own D10 posture): a row about a visitor or a
|
|
1323
|
-
// listing has none, and a ledger row must outlive anything it names.
|
|
1324
|
-
personId String? @map("person_id")
|
|
1325
|
-
// #8438 D2 — where the answer came from (CONSENT_SOURCES). NOT NULL with no
|
|
1326
|
-
// default after the migration: every new row must say. Rows stored before
|
|
1327
|
-
// this column read `legacy`.
|
|
1328
|
-
source String @db.Text
|
|
1329
|
-
// The magnet slug when `source` is `magnet`.
|
|
1330
|
-
sourceRef String? @map("source_ref") @db.VarChar(200)
|
|
1331
|
-
// The registry's policy version the person read. NULL only for `legacy` rows.
|
|
1332
|
-
policyVersion Int? @map("policy_version")
|
|
1333
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
1334
|
-
|
|
1335
|
-
// "What did this subject agree to" — every row for one subject, newest last.
|
|
1336
|
-
@@index([siteId, subjectType, subjectId])
|
|
1337
|
-
// "Who agreed to this purpose" — the cross-surface count the id exists for (D1/D2).
|
|
1338
|
-
@@index([siteId, purposeId])
|
|
1339
|
-
@@index([workspaceId])
|
|
1340
|
-
// #8438 D2 — "current state per purpose" for one Person (DISTINCT ON).
|
|
1341
|
-
@@index([workspaceId, personId, purposeId, createdAt])
|
|
1342
|
-
@@map("consent_grants")
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
// #8438 D2 — links a banner visitor (a random browser-held id) to the Person a
|
|
1346
|
-
// later form or sign-in identifies, so the Person's cookie history can include
|
|
1347
|
-
// the visitor's own rows without ever editing them. Append-only like the
|
|
1348
|
-
// ledger: a database trigger refuses any change to a stored row. No relation
|
|
1349
|
-
// directive (ADR-0009 invariant #1); no IP, user-agent or device column.
|
|
1350
|
-
model ConsentVisitorLink {
|
|
1351
|
-
id String @id @default(dbgenerated("('cvlink_'::text || (uuidv7())::text)")) @db.Text
|
|
1352
|
-
workspaceId String @map("workspace_id")
|
|
1353
|
-
visitorId String @map("visitor_id")
|
|
1354
|
-
personId String @map("person_id")
|
|
1355
|
-
source String @db.Text
|
|
1356
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
1357
|
-
|
|
1358
|
-
@@unique([workspaceId, visitorId, personId])
|
|
1359
|
-
@@index([workspaceId, personId])
|
|
1360
|
-
@@map("consent_visitor_links")
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
/// Appointment — a Cal.com booking, normalized from the webhook receiver (#6085, brief H7).
|
|
1364
|
-
///
|
|
1365
|
-
/// siteId-scoped with no FK, like every other inbox model (ADR 0009 D2). The
|
|
1366
|
-
/// (siteId, provider, providerBookingUid) unique constraint IS the idempotency
|
|
1367
|
-
/// key: every write is a guarded updateMany, and the create that follows a zero
|
|
1368
|
-
/// match races on THIS index — which is what makes "exactly one row per booking
|
|
1369
|
-
/// uid" a DATABASE property rather than a property of a process that may have
|
|
1370
|
-
/// restarted. `provider` is in the key because a deployment may one day run two
|
|
1371
|
-
/// providers against one receiver and their uid spaces do not coordinate (#4288).
|
|
1372
|
-
model Appointment {
|
|
1373
|
-
id String @id @default(dbgenerated("('appt_'::text || (uuidv7())::text)")) @db.Text
|
|
1374
|
-
siteId String @map("site_id") @db.VarChar(200)
|
|
1375
|
-
// #6374 — the tenancy fence, extended to this model (spec
|
|
1376
|
-
// docs/specs/6374-the-fence-schema-wave-spec.md D1). NOT NULL, no
|
|
1377
|
-
// Prisma relation directive (ADR-0009 invariant #1), and
|
|
1378
|
-
// appointments_workspace_site_agree CHECK (workspace_id = site_id) — since
|
|
1379
|
-
// siteId IS the resolved workspace id, Postgres itself rejects a row
|
|
1380
|
-
// whose two columns disagree. Every write site stamps a SERVER-RESOLVED
|
|
1381
|
-
// value, never one from a request body (AC-6).
|
|
1382
|
-
workspaceId String @map("workspace_id")
|
|
1383
|
-
/// Cal.com's own booking `uid` — stable across reschedules of the same booking.
|
|
1384
|
-
/// Bounded: a value above Postgres's btree entry limit cannot be indexed at all.
|
|
1385
|
-
providerBookingUid String @map("provider_booking_uid") @db.VarChar(200)
|
|
1386
|
-
/// Provider identifier, part of the unique key (#4288).
|
|
1387
|
-
provider String @default("cal") @db.VarChar(32)
|
|
1388
|
-
startTime DateTime @map("start_time")
|
|
1389
|
-
endTime DateTime @map("end_time")
|
|
1390
|
-
/// The event's own title as the provider stated it. PII-adjacent: never logged (NFR-1).
|
|
1391
|
-
title String? @db.VarChar(300)
|
|
1392
|
-
/// Free text typed by an anonymous prospect — bounded at the same 120/254 the
|
|
1393
|
-
/// outbound seam already bounds them at (packages/validation SchedulingAttendeeSchema).
|
|
1394
|
-
attendeeName String @map("attendee_name") @db.VarChar(120)
|
|
1395
|
-
attendeeEmail String @map("attendee_email") @db.VarChar(254)
|
|
1396
|
-
attendeeTimeZone String? @map("attendee_time_zone") @db.VarChar(64)
|
|
1397
|
-
/// Normalized lifecycle state — see § D5. One of: pending | confirmed | cancelled.
|
|
1398
|
-
status String @db.VarChar(32)
|
|
1399
|
-
/// The payment flag. BOOKING_PAID sets it true; nothing else in this issue reads it.
|
|
1400
|
-
paid Boolean @default(false)
|
|
1401
|
-
/// Set by BOOKING_PAYMENT_INITIATED; a checkout was opened on Cal's side.
|
|
1402
|
-
paymentInitiatedAt DateTime? @map("payment_initiated_at")
|
|
1403
|
-
/// Set by BOOKING_NO_SHOW_UPDATED.
|
|
1404
|
-
noShow Boolean @default(false) @map("no_show")
|
|
1405
|
-
cancelledAt DateTime? @map("cancelled_at")
|
|
1406
|
-
/// Three INDEPENDENT watermarks, one per event family (§ D7). One shared column
|
|
1407
|
-
/// forced a choice between dropping a late payment fact and letting a replayed
|
|
1408
|
-
/// one revert state; three columns make each family monotonic on its own terms.
|
|
1409
|
-
lifecycleEventAt DateTime @map("lifecycle_event_at")
|
|
1410
|
-
paymentEventAt DateTime? @map("payment_event_at")
|
|
1411
|
-
noShowEventAt DateTime? @map("no_show_event_at")
|
|
1412
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
1413
|
-
updatedAt DateTime @updatedAt @map("updated_at")
|
|
1414
|
-
/// #852 D1 — the Person of the attendee, in this row's workspace. Stamped
|
|
1415
|
-
/// at create (and on a spoke bind's move) by appointments.ts through
|
|
1416
|
-
/// findOrCreatePerson. Plain scalar, no @relation. NULL when the attendee
|
|
1417
|
-
/// email is not a storable address.
|
|
1418
|
-
personId String? @map("person_id")
|
|
1419
|
-
|
|
1420
|
-
@@unique([siteId, provider, providerBookingUid])
|
|
1421
|
-
@@index([siteId, startTime])
|
|
1422
|
-
@@index([createdAt])
|
|
1423
|
-
@@index([workspaceId])
|
|
1424
|
-
@@index([personId])
|
|
1425
|
-
@@map("appointments")
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
/// AppointmentOrigin — which workspace a booking made THROUGH A SPOKE belongs
|
|
1429
|
-
/// to (#7532, spec D5). Written by `POST /v1/schedule/book` the moment Cal
|
|
1430
|
-
/// answers, and consulted by the deployment-wide Cal.com webhook BEFORE the
|
|
1431
|
-
/// site alias, so a spoke's booking is stored in the spoke's workspace.
|
|
1432
|
-
///
|
|
1433
|
-
/// One origin per booking uid: the unique key leaves `site_id` out. No
|
|
1434
|
-
/// relation directive (ADR 0009) — `workspace_id` and `spoke_id` are plain
|
|
1435
|
-
/// columns with format CHECKs, joined in apps/api/src/services/.
|
|
1436
|
-
model AppointmentOrigin {
|
|
1437
|
-
id String @id @default(dbgenerated("('apptorg_'::text || (uuidv7())::text)")) @db.Text
|
|
1438
|
-
provider String @default("cal") @db.VarChar(32)
|
|
1439
|
-
providerBookingUid String @map("provider_booking_uid") @db.VarChar(200)
|
|
1440
|
-
workspaceId String @map("workspace_id")
|
|
1441
|
-
spokeId String @map("spoke_id")
|
|
1442
|
-
createdAt DateTime @default(now()) @map("created_at")
|
|
1443
|
-
|
|
1444
|
-
@@unique([provider, providerBookingUid])
|
|
1445
|
-
@@index([workspaceId])
|
|
1446
|
-
@@map("appointment_origins")
|
|
1447
|
-
}
|