From 895ef913f3a18877d1e87178573ffa6a7510d5b9 Mon Sep 17 00:00:00 2001 From: Bailey Pumfleet Date: Wed, 4 Nov 2026 15:02:21 -0600 Subject: [PATCH 1/9] Gate Scheduling by plan, extend billing holds, and make HIPAA contract-only Scheduling can't charge more than $0.10 a booking, so serious use pays through plans. Live environments only; sandbox has everything on every plan: - hosts: 3 on Free, 25 on Pro, 100 included on Scale then $0.99 a month each (a new `scheduling.hosts` meter, charged by the day like inboxes, first priced on the January 2027 card; existing live hosts wait until then) - holds on Pro and up; the embedded host portal, attribute routing and pages without "Scheduling by coss.com" on Scale and up - seats per group session, hosts per availability search, and a plan rate for live slot lists and searches What already exists keeps working; only new setup meets the plan. Billing holds and project budgets now also stop new live bookings, signature requests and inboxes, and close hosted forms. Holds already made still confirm. People booking or submitting never hear about the customer's billing. Price SMS received on Canadian numbers from January 2027: numbers are sold there but inbound was invoiced as an uncharged line. HIPAA mode needs a sales or internal contract, and is on the public site: the security page, the plan table, the pricing FAQ and the Nylas comparison, which also lists customers' own OAuth apps and booking domains as coming soon. Remove the video meter, rate and product, which never shipped. The dashboard's price list also drops meters without a price (Sign's blank row). --- .changeset/scheduling-hosts-meter.md | 5 + AGENTS.md | 21 +- apps/api/openapi.json | 287 +- apps/api/src/app.ts | 3 +- apps/api/src/errors.ts | 10 + apps/api/src/middleware/rate-limit.ts | 24 + apps/api/src/routes/scheduling-client.ts | 66 +- apps/api/src/routes/scheduling.ts | 48 +- apps/api/src/routes/sign.ts | 6 + apps/api/test/scheduling-plans.test.ts | 431 + apps/scheduling/AGENTS.md | 3 +- .../src/app/b/[sessionId]/booker.tsx | 3 + .../src/app/h/[sessionId]/host-portal.tsx | 2 + .../src/app/m/[sessionId]/manage.tsx | 2 + apps/scheduling/src/components/page.tsx | 19 +- apps/scheduling/src/lib/branding.ts | 2 + apps/scheduling/test/attribution.test.tsx | 22 + apps/site/content/docs/(platform)/billing.mdx | 6 +- apps/site/content/docs/(platform)/hipaa.mdx | 6 +- apps/site/content/docs/(platform)/spend.mdx | 5 +- .../site/content/docs/scheduling/bookings.mdx | 2 +- .../content/docs/scheduling/embedding.mdx | 4 + apps/site/content/docs/scheduling/hosts.mdx | 4 + .../docs/scheduling/pools-and-groups.mdx | 7 +- apps/site/content/docs/scheduling/routing.mdx | 3 +- apps/site/content/products/scheduling.mdx | 23 +- apps/site/content/products/security.mdx | 15 + apps/site/content/solutions/booking.mdx | 4 +- apps/site/src/app/(home)/pricing/page.tsx | 3 +- .../src/components/pricing/pricing-faq.tsx | 15 + apps/site/src/lib/billing-text.ts | 18 +- apps/site/src/lib/compare-data/trust.ts | 29 +- apps/site/src/lib/pricing.ts | 6 +- apps/site/test/billing-text.test.ts | 29 +- .../[envSlug]/scheduling/actions.ts | 20 +- .../[envSlug]/scheduling/hosts/page.tsx | 38 +- .../[orgSlug]/settings/billing/plans/page.tsx | 2 +- apps/web/src/app/admin/actions.ts | 8 +- .../v1/orgs/[orgSlug]/billing/plans/wire.ts | 2 +- apps/web/src/lib/billing-preflight.ts | 13 +- apps/web/src/lib/billing.ts | 8 + apps/web/src/lib/scheduling.ts | 2 + apps/web/test/admin-actions.test.ts | 11 +- apps/web/test/mobile-api.test.ts | 2 +- apps/worker/src/jobs.ts | 10 + packages/billing/AGENTS.md | 5 +- packages/billing/src/catalog/entitlements.ts | 15 + packages/billing/src/catalog/meters.ts | 21 +- packages/billing/src/catalog/plans.ts | 50 +- packages/billing/src/catalog/rates.ts | 15 +- .../sms-prices-2027-01-01.generated.ts | 1 + packages/billing/src/enforcement.ts | 17 +- packages/billing/src/gate.ts | 2 + packages/billing/src/metering.ts | 30 +- packages/billing/test/engine.test.ts | 46 +- packages/compliance/AGENTS.md | 5 +- packages/db/migrations/0128_chemical_echo.sql | 5 + .../db/migrations/meta/0128_snapshot.json | 31202 ++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + packages/db/src/schema/scheduling.ts | 6 + packages/email/src/errors.ts | 2 + packages/email/src/inboxes.ts | 57 +- packages/email/test/contacts.test.ts | 3 + packages/forms/src/submit.ts | 12 +- packages/migrate/test/runner.test.ts | 3 + packages/ratelimit/src/buckets.ts | 10 + packages/scheduling/AGENTS.md | 27 + packages/scheduling/src/availability.ts | 8 +- packages/scheduling/src/bookings.ts | 8 +- packages/scheduling/src/errors.ts | 4 + packages/scheduling/src/event-types.ts | 64 +- packages/scheduling/src/hosts.ts | 214 +- packages/scheduling/src/index.ts | 1 + packages/scheduling/src/plan.ts | 51 + packages/scheduling/src/sessions.ts | 5 + packages/scheduling/src/types.ts | 4 +- packages/sdk/src/types/billing.ts | 2 +- packages/sign/src/errors.ts | 4 + packages/sign/src/requests.ts | 30 + packages/sms/test/notifications.test.ts | 3 + packages/sms/test/provisioning.test.ts | 6 + 81 files changed, 33024 insertions(+), 170 deletions(-) create mode 100644 .changeset/scheduling-hosts-meter.md create mode 100644 apps/api/test/scheduling-plans.test.ts create mode 100644 apps/scheduling/test/attribution.test.tsx create mode 100644 packages/db/migrations/0128_chemical_echo.sql create mode 100644 packages/db/migrations/meta/0128_snapshot.json create mode 100644 packages/scheduling/src/plan.ts diff --git a/.changeset/scheduling-hosts-meter.md b/.changeset/scheduling-hosts-meter.md new file mode 100644 index 000000000..b67fcd1a9 --- /dev/null +++ b/.changeset/scheduling-hosts-meter.md @@ -0,0 +1,5 @@ +--- +"coss": patch +--- + +Billing: the `scheduling.hosts` meter is new, and the unreleased `video.participant_minutes` is gone. diff --git a/AGENTS.md b/AGENTS.md index 56b5cc621..873d9b28d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,6 @@ # AGENTS.md — coss.com monorepo -coss.com is a unified developer platform: one API key + one SDK (`coss.email.send()`, `coss.sms.send()`, later -video, scheduling…). This repo holds the dashboard, the public API, shared packages and the public SDK. +coss.com is a unified developer platform: one API key + one SDK (`coss.email.send()`, `coss.sms.send()`, `coss.scheduling`…). This repo holds the dashboard, the public API, shared packages and the public SDK. Email delivery goes through the provider interface in `packages/email/src/provider.ts` (Amazon SES v2 with one SES tenant per organization, or the fake provider); sandbox environments always use the fake provider. Delivery events drive per-recipient status and signed customer webhooks. Environments also receive email (an SES receipt rule → S3 + @@ -92,7 +91,7 @@ billing enqueues `billing.*` events through it — and email imports only `./met `dashboard-routes` (no deps) ← `web`, `tenancy`, `email`, `sms`, `reputation`, `billing`, `forms`, `mobile` (paths of dashboard pages); `retention` (no deps) ← `tenancy`, `webhooks`, `reputation`, `screening`, `email`, `hooks`, `billing`, `push`, `notifications`, `sign`, `verify`, `forms`, `worker`, `web`, `site`, `mobile`; `db ← logs ← webhooks ← verify` (`verify` also `→ sms, risk, reputation, ratelimit, signals, retention, versioning` and only `@coss/billing/{metering,gate,catalog}`; `email` imports only `@coss/verify/schemas`, for `WEBHOOK_EVENT_TYPES`); -`db ← logs ← webhooks ← sign` (`sign` also `→ versioning, retention` and only `@coss/billing/metering`; `email` imports only `@coss/sign/schemas`, for `WEBHOOK_EVENT_TYPES`); `db ← logs ← webhooks ← scheduling` (`scheduling` also `→ email-layout, versioning, retention` and only `@coss/billing/{metering,gate}`; never `@coss/email` or `@coss/sms`: sending is injected; `email` imports only `@coss/scheduling/schemas`, for `WEBHOOK_EVENT_TYPES`); `db ← notifications ← support` (`support` also `→ signals, email-layout, retention` and only `@coss/billing/catalog`; never `@coss/email`: mail, reply text and the plan lookup are injected); `db ← logs ← webhooks ← risk ← forms` (`forms` also `→ versioning, retention` and only `@coss/billing/metering`; never `@coss/email`: mail and contacts are injected; `email` imports only `@coss/forms/schemas`, for `WEBHOOK_EVENT_TYPES`); `api → tenancy, email, sms, sign, verify, forms, hooks, reputation, webhooks, ratelimit, versioning, logs, billing, risk, screening (`./env` only), db`; +`db ← logs ← webhooks ← sign` (`sign` also `→ versioning, retention` and only `@coss/billing/metering`; `email` imports only `@coss/sign/schemas`, for `WEBHOOK_EVENT_TYPES`); `db ← logs ← webhooks ← scheduling` (`scheduling` also `→ email-layout, versioning, retention` and only `@coss/billing/{metering,gate,catalog}`; never `@coss/email` or `@coss/sms`: sending is injected; `email` imports only `@coss/scheduling/schemas`, for `WEBHOOK_EVENT_TYPES`); `db ← notifications ← support` (`support` also `→ signals, email-layout, retention` and only `@coss/billing/catalog`; never `@coss/email`: mail, reply text and the plan lookup are injected); `db ← logs ← webhooks ← risk ← forms` (`forms` also `→ versioning, retention` and only `@coss/billing/metering`; never `@coss/email`: mail and contacts are injected; `email` imports only `@coss/forms/schemas`, for `WEBHOOK_EVENT_TYPES`); `api → tenancy, email, sms, sign, verify, forms, hooks, reputation, webhooks, ratelimit, versioning, logs, billing, risk, screening (`./env` only), db`; `smtp → tenancy, email, ratelimit, logs, billing, observability, db`; `web → auth, tenancy, email, sms, sign, forms, support, hooks, reputation, screening, webhooks, ratelimit, versioning, retention, logs, billing, risk, push, migrate, companion, db, ui`; `mobile → companion` (types only; a dev dependency), `retention`, `observability`, `dashboard-routes` (bundled by Metro: keep them dependency-free); `portal → hooks, webhooks, ratelimit, logs, observability, db, ui`; `sign-app → sign, ratelimit, observability, db, ui`; `site → ui, auth, versioning, retention, billing, risk, reputation, ratelimit, sms, sdkgen` (only the client-safe `@coss/auth/{slugs,constants,api-key-permissions}`, the changelog, the retention policy, `@coss/billing/{catalog,money,engine}`, `@coss/risk/schemas`, `@coss/reputation/schemas` (the default policy), `@coss/ratelimit/buckets` (the bucket catalog, no imports), `@coss/sms/segments` (the segment calculator) and `@coss/sdkgen`'s `snippets.json` + `./methods`) + `@coss/api/openapi.json` (data only, never api code); `sdkgen → @coss/api/openapi.json` (data only); `infra → auth, email, sms, sign, hooks, ratelimit, screening, billing` (`slugs`, `env` only); @@ -412,7 +411,7 @@ parent(id, organization_id)` and the `createdAt()`/`updatedAt()` helpers (histor Auth hard-deletes keys); `bun run db:generate`, review and commit the SQL + `migrations/meta/`, `bun run db:migrate`. Never edit an applied migration; do renames as a separate step (drizzle-kit prompts interactively). -**Add a product** (e.g. `video`; `packages/scheduling` is the most recent one built this way): +**Add a product** (`packages/scheduling` is the most recent one built this way): 1. Tables + ID prefix (above). 2. `packages/`: `src/schemas.ts` (zod `strictObject`, snake_case, `.meta({ id })`, no DB imports, exported as `./schemas`), provider interface + fake provider, a service taking the authenticated environment, an error class with @@ -440,8 +439,7 @@ Never edit an applied migration; do renames as a separate step (drizzle-kit prom (only for rows actually inserted, so deduplicated events aren't logged twice), and add a `LogSource` value (enum migration + `LOG_SOURCES` + SDK `LogSource`). Mutations of the product's settings record `recordAudit` (dashboard) / `recordApiAudit` (API) audit events. -10. Billing: its meter is in `METERS` and priced in `RATE_CARDS` (`@coss/billing/catalog`; video and scheduling - already are). Call `recordUsage(tx, …)` from `@coss/billing/metering` once per billable event, in the transaction +10. Billing: its meter goes in `METERS` and is priced in `RATE_CARDS` (`@coss/billing/catalog`). Call `recordUsage(tx, …)` from `@coss/billing/metering` once per billable event, in the transaction that makes it billable (guard it with a compare-and-set, like `email.usage_recorded_at`), and reserve capped usage at acceptance through an injected `UsageGate` (as `EmailServiceOptions.billing` does). Plan-gated creation takes an injected `PlanGate`. @@ -531,7 +529,7 @@ notification emails) and, if there is useful advice, to `ADVICE` (`packages/repu `recordOpen`/`recordClick` (from the email's tracking token), `trackingBaseUrlFor` (dispatch; reads domain names across orgs like `authorizeLiveSender`), `receiveEmail`/`routeRecipients` (receiving; from the envelope recipients), `recordSmsEvent`/`ingestSmsEvents` (from the `sms_id` of the callback URL), `receiveSms` (from the receiving number), `handleStatusCallback`/`handleInboundCallback` (from the callback's subaccount), `authorizeLiveSmsSender`, `ingestDmarcReports` (from the report address's token), `reencryptWebhookSecrets`, `reencryptSchedulingData` (operator scripts), `authenticateHookPortalSession` (from the session), `ingestSupportEmails` (one environment, across senders) and the rest of `@coss/support`'s worker functions, `submitForm`/`getHostedForm` (from the form id of `/f/:id`), `confirmFormSubmission`/`findConfirmation` (from the verification token), and the worker scans `runDueWebhookDeliveries`, `runDueLogDrainDeliveries`, `runDueHookDeliveries`, `parkHeldHookDeliveries`, `runDueHookRecoveries`, the Hooks purges, `recheckDueDomains`, `recheckDueTrackingDomains`, `recheckDueReceivingDomains`, `reconcileDedicatedIpPools`, `sweepStaleClaims`, `sweepUnclaimedEmails`, `runDueReputationEvaluations`, `runDueSubaccountEvaluations`, `runDueOrganizationDeletions`, `runDueOrganizationLogos`, `runDueSubaccountDeletions`, `runDueDmarcNotifications`, `runDueMigrations`, `EmailService.dispatchDue`, - `unclaimedEmailBacklog`, `unclaimedSmsBacklog` (the `dispatch-backlog` loop: counts only), `SmsService.dispatchDue`, `sweepStaleSmsClaims`, `sweepUnclaimedSms`, `advanceDueSmsRegistrations`, `chargeDueSmsFees`, `chargeDueInboxFees`, `recordDueVerifySmsUsage` (bills each organization for its codes from coss.com's shared numbers, reading coss.com's messages), + `unclaimedEmailBacklog`, `unclaimedSmsBacklog` (the `dispatch-backlog` loop: counts only), `SmsService.dispatchDue`, `sweepStaleSmsClaims`, `sweepUnclaimedSms`, `advanceDueSmsRegistrations`, `chargeDueSmsFees`, `chargeDueInboxFees`, `chargeDueHostFees`, `recordDueVerifySmsUsage` (bills each organization for its codes from coss.com's shared numbers, reading coss.com's messages), `authenticateSchedulingSession` (from the token), `handleCalendarPush`, `completeCalendarAuthorization` (from the push's channel and the consent's state), `handleZoomDeauthorization` (from Zoom's signed event), `refreshIdleZoomConnections`, `runDueCalendarSyncs`, `runDueCalendarWrites`, `renewDueCalendarChannels`, `checkHostEditedEvents`, `runDueSchedulingNotifications`, `expireDueBookings`, `runDueFormDeliveries`, `runDueFormStoreCleanup` (and the dispatch jobs, from the job's `organizationId`); `@coss/sms/admin` is platform-level like `@coss/email/admin`. Server actions take only slugs + @@ -701,14 +699,16 @@ notification emails) and, if there is useful advice, to `ADVICE` (`packages/repu published; `/f/*` is served only on `FORMS_PUBLIC_URL`'s host in production, never coss.com's. - **Scheduling** (`packages/scheduling/AGENTS.md`): only `claims.ts` writes claims, in the fixed lock order; slots are computed only in Bun, through `isBookable`; calendars contribute busy intervals and nothing else, and an - unreadable one never reads as free; answers never leave `getBooking`; coss.com never auto-cancels a booking. + unreadable one never reads as free; answers never leave `getBooking`; coss.com never auto-cancels a booking; + plan gates and billing holds apply to live environments only, and refuse new setup and new bookings, never what + already exists. - **Support** (`packages/support/AGENTS.md`): `From` proves nothing, so a match never grants access by itself: customers see only a confirmed organization's tickets, an unverified message stays rep-only, we always reply to `From` and never copy inbound Cc from unauthenticated mail; notes never leave `@coss/support/admin`; threads never match on a subject. - **HIPAA mode** (`packages/compliance/AGENTS.md`): a new vendor or a new path that sends customer content outside AWS needs a gate and an entry in `HIPAA_VENDORS`; a new sender that can fall back to coss.com's - environment passes `hipaaOrigin` for organizations in the mode; it is never a billing entitlement. + environment passes `hipaaOrigin` for organizations in the mode; it is contract-only (a sales contract to turn it on) but never a billing entitlement (billing never turns it off). - **Organization deletion** (`packages/tenancy/AGENTS.md`): a table with many rows per organization belongs in `deleteOrganizationData`, anything held outside Postgres in `releaseProviderResources`. @@ -819,8 +819,7 @@ editing an API key's permissions (create a new key), API keys restricted to send uploading an organization logo (logos are only found automatically), uploading a profile picture, email change, email/SMS two-factor codes (through Verify), sign-in providers other than Google and GitHub, "trust this device" for 2FA, renaming passkeys, webhooks and notification emails for dedicated IP status changes, standard (manually warmed) dedicated IPs or more than one pool per organization, an automatic direct-debit policy (only per-account -required payment methods), multi-currency, metering for video (its meter -and rate exist; the product doesn't), SMS alphanumeric sender IDs, short codes, MMS, batch SMS, SMS templates, +required payment methods), multi-currency, SMS alphanumeric sender IDs, short codes, MMS, batch SMS, SMS templates, sole-proprietor 10DLC brands, `sms.registration.*` webhooks and push notifications, Twilio usage reconciliation, and these Hooks features: channels, payload transformations, custom endpoint headers, sending one message to several applications, strict ordering, portal custom domains, per-destination rate-limit overrides, Hooks attempts in diff --git a/apps/api/openapi.json b/apps/api/openapi.json index 683a4e773..9554c7c1d 100644 --- a/apps/api/openapi.json +++ b/apps/api/openapi.json @@ -1619,8 +1619,8 @@ "verify.sms", "forms.submissions", "forms.file_uploads", - "video.participant_minutes", "scheduling.bookings", + "scheduling.hosts", "hooks.messages", null ], @@ -2279,8 +2279,8 @@ "verify.sms", "forms.submissions", "forms.file_uploads", - "video.participant_minutes", "scheduling.bookings", + "scheduling.hosts", "hooks.messages", null ], @@ -2308,8 +2308,8 @@ "verify.sms", "forms.submissions", "forms.file_uploads", - "video.participant_minutes", "scheduling.bookings", + "scheduling.hosts", "hooks.messages" ], "description": "What is counted, e.g. `email.recipients`." @@ -26459,8 +26459,8 @@ "verify.sms", "forms.submissions", "forms.file_uploads", - "video.participant_minutes", "scheduling.bookings", + "scheduling.hosts", "hooks.messages" ], "description": "What is counted, e.g. `email.recipients`." @@ -26839,8 +26839,8 @@ "verify.sms", "forms.submissions", "forms.file_uploads", - "video.participant_minutes", "scheduling.bookings", + "scheduling.hosts", "hooks.messages" ], "description": "What is counted, e.g. `email.recipients`." @@ -27260,8 +27260,8 @@ "verify.sms", "forms.submissions", "forms.file_uploads", - "video.participant_minutes", "scheduling.bookings", + "scheduling.hosts", "hooks.messages", null ], @@ -56162,6 +56162,27 @@ } } }, + "402": { + "description": "In live environments: an overdue invoice or spend limit (`billing_hold`), or the project's budget (`budget_exceeded`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -57134,6 +57155,27 @@ } } }, + "402": { + "description": "In live environments: an overdue invoice or spend limit (`billing_hold`), or the project's budget (`budget_exceeded`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -64714,6 +64756,27 @@ } } }, + "402": { + "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -67145,6 +67208,27 @@ } } }, + "402": { + "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -67860,6 +67944,27 @@ } } }, + "402": { + "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -68176,6 +68281,27 @@ } } }, + "402": { + "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -68407,6 +68533,27 @@ } } }, + "402": { + "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -68693,6 +68840,27 @@ } } }, + "402": { + "description": "In live environments: an overdue invoice or spend limit (`billing_hold`), the project's budget (`budget_exceeded`), the plan's allowance (`usage_limit_exceeded`) or what it includes (`plan_feature_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -70998,6 +71166,27 @@ } } }, + "402": { + "description": "In live environments: an overdue invoice or spend limit (`billing_hold`), the project's budget (`budget_exceeded`), the plan's allowance (`usage_limit_exceeded`) or what it includes (`plan_feature_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -71809,6 +71998,27 @@ } } }, + "402": { + "description": "In live environments: an overdue invoice or spend limit (`billing_hold`), the project's budget (`budget_exceeded`), the plan's allowance (`usage_limit_exceeded`) or what it includes (`plan_feature_unavailable`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -72082,6 +72292,27 @@ } } }, + "402": { + "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -74530,6 +74761,27 @@ } } }, + "402": { + "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -74782,6 +75034,27 @@ } } }, + "402": { + "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { @@ -90768,8 +91041,8 @@ "verify.sms", "forms.submissions", "forms.file_uploads", - "video.participant_minutes", "scheduling.bookings", + "scheduling.hosts", "hooks.messages" ], "description": "Only this meter." diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index a8540691b..2b392e23a 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -291,7 +291,7 @@ export function createApp(opts: CreateAppOptions) { app.route( SCHEDULING_CLIENT_PATH, createSchedulingClientRoutes({ - deps: { ...opts.scheduling, billing: opts.billing }, + deps: { ...opts.scheduling, billing: opts.billing, plans: opts.plans }, hostedUrl: opts.scheduling.publicUrl ?? null, }), ); @@ -341,6 +341,7 @@ export function createApp(opts: CreateAppOptions) { scheduling: opts.scheduling && { ...opts.scheduling, billing: opts.billing, + plans: opts.plans, screenSetup: opts.scheduling.screenSetup ?? hostedPageScreening(opts.riskDeps), }, riskDeps: opts.riskDeps, diff --git a/apps/api/src/errors.ts b/apps/api/src/errors.ts index ad5eeb684..c39f936f1 100644 --- a/apps/api/src/errors.ts +++ b/apps/api/src/errors.ts @@ -308,6 +308,9 @@ function fromSmsError(error: SmsError): ApiError { function fromSignError(error: SignError): ApiError { switch (error.code) { + case "billing_hold": + case "budget_exceeded": + return new ApiError(402, { type: "billing_error", code: error.code, message: error.message }); case "request_in_progress": case "idempotency_key_reused": return fromIdempotencyError({ code: error.code, message: error.message }); @@ -398,6 +401,10 @@ function fromFormError(error: FormError): ApiError { function fromSchedulingError(error: SchedulingError): ApiError { switch (error.code) { + case "billing_hold": + case "budget_exceeded": + // Paying or raising the budget is the fix: 402, never retried by the SDK. + return new ApiError(402, { type: "billing_error", code: error.code, message: error.message }); case "not_found": return new ApiError(404, { type: "not_found_error", @@ -480,6 +487,9 @@ function fromContactError(error: ContactError): ApiError { } function fromInboxError(error: InboxError): ApiError { + if (error.code === "billing_hold" || error.code === "budget_exceeded") { + return new ApiError(402, { type: "billing_error", code: error.code, message: error.message }); + } if (error.code.endsWith("not_found")) { return new ApiError(404, { type: "not_found_error", diff --git a/apps/api/src/middleware/rate-limit.ts b/apps/api/src/middleware/rate-limit.ts index d08014196..02944634e 100644 --- a/apps/api/src/middleware/rate-limit.ts +++ b/apps/api/src/middleware/rate-limit.ts @@ -29,6 +29,30 @@ export function rateLimitError( return new ApiError(429, { type: "rate_limit_error", ...error }, headers); } +/** + * Scheduling's live slot lists and searches, per organization, at the plan's rate: what a booking + * page or a marketplace's search costs. Sandbox environments keep only their key's own bucket. + */ +export async function limitSchedulingSlots( + rateLimiter: AppEnv["Variables"]["rateLimiter"], + env: { organizationId: string; mode: "live" | "sandbox" }, +): Promise { + if (env.mode !== "live") return; + const decision = await rateLimiter.consume({ + buckets: ["scheduling.slots.second"], + subject: env.organizationId, + organizationId: env.organizationId, + }); + if (!decision.allowed) { + throw rateLimitError(decision, { + code: "scheduling_rate_limited", + message: + "Too many slot lists and searches for this organization. Retry after `Retry-After` " + + "seconds; higher plans allow more.", + }); + } +} + /** * Route-level middleware, after `requireApiKey`: counts the request against the key's bucket for * this route class (`api.read`, `api.write`, `api.send`), with the organization's overrides. diff --git a/apps/api/src/routes/scheduling-client.ts b/apps/api/src/routes/scheduling-client.ts index a4f423ad9..c9ac55119 100644 --- a/apps/api/src/routes/scheduling-client.ts +++ b/apps/api/src/routes/scheduling-client.ts @@ -54,6 +54,7 @@ import { import { type Context, Hono, type MiddlewareHandler } from "hono"; import { z } from "zod"; import { ApiError } from "../errors.ts"; +import { limitSchedulingSlots } from "../middleware/rate-limit.ts"; import type { AppEnv } from "../types.ts"; export const SCHEDULING_CLIENT_PATH = "/scheduling/client"; @@ -132,6 +133,24 @@ export function createSchedulingClientRoutes(opts: { await next(); }); + /** The person booking isn't the customer: their billing stays out of what the page says. */ + const bookingForPerson = async (run: () => Promise): Promise => { + try { + return await run(); + } catch (error) { + if ( + error instanceof SchedulingError && + (error.code === "billing_hold" || error.code === "budget_exceeded") + ) { + throw new SchedulingError( + "account_suspended", + "This account can't take bookings right now.", + ); + } + throw error; + } + }; + const authenticated: MiddlewareHandler = async (c, next) => { const byAddress = await c.var.rateLimiter.consume({ buckets: ["scheduling.ip"], @@ -229,6 +248,10 @@ export function createSchedulingClientRoutes(opts: { brand_name: settings?.brandName ?? null, accent_color: settings?.accentColor ?? null, logo: logo ? { data_url: logoDataUrl(logo), width: logo.width, height: logo.height } : null, + // "Scheduling by coss.com" on the page, on plans without the white label. + coss_attribution: deps.plans + ? !(await deps.plans.hasFeature(env.organizationId, "schedulingWhiteLabel")) + : false, }; }; @@ -290,8 +313,9 @@ export function createSchedulingClientRoutes(opts: { eventType: string, hostRefs: readonly string[] | undefined, attributes?: Record, - ) => - slotList( + ) => { + await limitSchedulingSlots(c.var.rateLimiter, c.var.session.env); + return slotList( c, await listEventTypeSlots(c.var.session.env, { ...slotWindow(c), @@ -300,9 +324,11 @@ export function createSchedulingClientRoutes(opts: { attributes, }), ); + }; /** Where this booking could move to: its own hosts, length and buffers. */ const rescheduleSlots = async (c: Context, id: string) => { + await limitSchedulingSlots(c.var.rateLimiter, c.var.session.env); const { fromMs, toMs, nowMs } = slotWindow(c); return slotList(c, await listRescheduleSlots(c.var.session.env, id, { fromMs, toMs, nowMs })); }; @@ -323,23 +349,25 @@ export function createSchedulingClientRoutes(opts: { const fixed = fixedAttendee(session, deps); const attendee = fixed ?? body.attendee; if (!attendee) throw invalidBody("`attendee` is required.", "attendee"); - const { booking, replayed } = await createBooking( - env, - { - eventType, - startMs: instant(body.start, "start"), - host: session.hostId ?? undefined, - attributes: sessionAttributes(session, deps), - durationMinutes: body.duration_minutes, - attendee, - guests: body.guests, - answers: body.answers, - location: body.location, - locationFromBrowser: true, - idempotencyKey: c.req.header("idempotency-key"), - }, - deps, - sessionActor(session), + const { booking, replayed } = await bookingForPerson(() => + createBooking( + env, + { + eventType, + startMs: instant(body.start, "start"), + host: session.hostId ?? undefined, + attributes: sessionAttributes(session, deps), + durationMinutes: body.duration_minutes, + attendee, + guests: body.guests, + answers: body.answers, + location: body.location, + locationFromBrowser: true, + idempotencyKey: c.req.header("idempotency-key"), + }, + deps, + sessionActor(session), + ), ); if (replayed) c.header("Idempotent-Replayed", "true"); const emailed = diff --git a/apps/api/src/routes/scheduling.ts b/apps/api/src/routes/scheduling.ts index 5fa7d1038..39ac3db77 100644 --- a/apps/api/src/routes/scheduling.ts +++ b/apps/api/src/routes/scheduling.ts @@ -41,6 +41,7 @@ import { listSchedules, markNoShow, previewSeries, + requireRouting, rescheduleBooking, type SchedulingDeps, SchedulingError, @@ -108,7 +109,7 @@ import { recordApiAudit } from "../audit.ts"; import { ApiError } from "../errors.ts"; import { requireApiKey } from "../middleware/api-key.ts"; import { upgradeRequestBody } from "../middleware/api-version.ts"; -import { rateLimit } from "../middleware/rate-limit.ts"; +import { limitSchedulingSlots, rateLimit } from "../middleware/rate-limit.ts"; import type { AppEnv } from "../types.ts"; import { commonErrors, errorDoc, jsonDoc, requestIdHeader } from "./docs.ts"; import { idempotencyHeaders, idempotencyHook, idempotentReplayedHeader } from "./idempotency.ts"; @@ -165,6 +166,15 @@ const bodyErrors = { 400: errorDoc("The body is not valid JSON (`invalid_json`)."), 415: errorDoc("The body is not sent as `application/json` (`unsupported_media_type`)."), }; +const planRefused = errorDoc( + "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past " + + "the plan's limit (`plan_limit_reached`).", +); +const bookingRefused = errorDoc( + "In live environments: an overdue invoice or spend limit (`billing_hold`), the project's budget " + + "(`budget_exceeded`), the plan's allowance (`usage_limit_exceeded`) or what it includes " + + "(`plan_feature_unavailable`).", +); const notFound = errorDoc("Nothing with this id or uid exists in the environment (`not_found`)."); const invalid = errorDoc( "The request failed validation (`validation_error`, see `issues`), or can't be done: see `code`.", @@ -229,6 +239,7 @@ export const createHostRoute = createRoute({ body: { required: true, content: { "application/json": { schema: CreateHostRequest } } }, }, responses: { + 402: planRefused, 201: jsonDoc("The host.", HostObject), ...commonErrors, ...bodyErrors, @@ -412,6 +423,7 @@ export const createEventTypeRoute = createRoute({ body: { required: true, content: { "application/json": { schema: CreateEventTypeRequest } } }, }, responses: { + 402: planRefused, 201: jsonDoc("The event type.", EventTypeObject), ...commonErrors, ...bodyErrors, @@ -466,6 +478,7 @@ export const updateEventTypeRoute = createRoute({ body: { required: true, content: { "application/json": { schema: UpdateEventTypeRequest } } }, }, responses: { + 402: planRefused, 200: jsonDoc("The event type.", EventTypeObject), ...commonErrors, ...bodyErrors, @@ -487,6 +500,7 @@ export const listSlotsRoute = createRoute({ middleware: read(), request: { params: EventTypeParams, query: SlotsQuery }, responses: { + 402: planRefused, 200: jsonDoc("The slots.", SlotList), ...commonErrors, 404: notFound, @@ -515,6 +529,7 @@ export const searchAvailabilityRoute = createRoute({ }, }, responses: { + 402: planRefused, 200: jsonDoc("Hosts with slots.", AvailabilitySearchResult), ...commonErrors, ...bodyErrors, @@ -539,6 +554,7 @@ export const createBookingRoute = createRoute({ body: { required: true, content: { "application/json": { schema: CreateBookingRequest } } }, }, responses: { + 402: bookingRefused, 201: createdBooking("The booking."), ...commonErrors, ...bodyErrors, @@ -711,6 +727,7 @@ export const createHoldRoute = createRoute({ body: { required: true, content: { "application/json": { schema: CreateHoldRequest } } }, }, responses: { + 402: bookingRefused, 201: createdBooking("The held booking."), ...commonErrors, ...bodyErrors, @@ -777,6 +794,7 @@ export const createSeriesRoute = createRoute({ body: { required: true, content: { "application/json": { schema: CreateSeriesRequest } } }, }, responses: { + 402: bookingRefused, 201: createdBooking("The series and its bookings.", SeriesResult), ...commonErrors, ...bodyErrors, @@ -803,6 +821,7 @@ export const previewSeriesRoute = createRoute({ body: { required: true, content: { "application/json": { schema: CreateSeriesRequest } } }, }, responses: { + 402: planRefused, 200: jsonDoc("Each occurrence.", SeriesPreview), ...commonErrors, ...bodyErrors, @@ -1081,6 +1100,7 @@ export const createHostSessionRoute = createRoute({ body: { required: true, content: { "application/json": { schema: CreateHostSessionRequest } } }, }, responses: { + 402: planRefused, 201: jsonDoc("The session.", CreatedSessionBody), ...commonErrors, ...bodyErrors, @@ -1106,6 +1126,7 @@ export const createBookerSessionRoute = createRoute({ }, }, responses: { + 402: planRefused, 201: jsonDoc("The session.", CreatedSessionBody), ...commonErrors, ...bodyErrors, @@ -1237,7 +1258,11 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA }); router.openapi(createHostRoute, async (c) => { - const host = await createHost(c.var.auth, c.req.valid("json"), { screen: deps.screenSetup }); + const host = await createHost(c.var.auth, c.req.valid("json"), { + screen: deps.screenSetup, + plans: deps.plans, + billing: deps.billing, + }); await recordApiAudit(c, { action: "scheduling_host.created", target: { type: "scheduling_host", id: host.id, label: host.name }, @@ -1266,7 +1291,9 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA }); router.openapi(deleteHostRoute, async (c) => { - const { id } = await deleteHost(c.var.auth, c.req.valid("param").host); + const { id } = await deleteHost(c.var.auth, c.req.valid("param").host, { + billing: deps.billing, + }); await recordApiAudit(c, { action: "scheduling_host.deleted", target: { type: "scheduling_host", id }, @@ -1302,6 +1329,7 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA router.openapi(createEventTypeRoute, async (c) => { const eventType = await createEventType(c.var.auth, c.req.valid("json"), { screen: deps.screenSetup, + plans: deps.plans, }); await recordApiAudit(c, { action: "scheduling_event_type.created", @@ -1324,7 +1352,7 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA c.var.auth, c.req.valid("param").event_type, c.req.valid("json"), - { screen: deps.screenSetup }, + { screen: deps.screenSetup, plans: deps.plans }, ); await recordApiAudit(c, { action: "scheduling_event_type.updated", @@ -1335,6 +1363,8 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA router.openapi(listSlotsRoute, async (c) => { const query = c.req.valid("query"); + await limitSchedulingSlots(c.var.rateLimiter, c.var.auth); + await requireRouting(c.var.auth, deps.plans, attributesParam(query.attributes)); const slots = await listEventTypeSlots(c.var.auth, { eventTypeRef: c.req.valid("param").event_type, fromMs: instant(query.start, "start"), @@ -1360,6 +1390,9 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA router.openapi(searchAvailabilityRoute, async (c) => { const body = c.req.valid("json"); + await limitSchedulingSlots(c.var.rateLimiter, c.var.auth); + await requireRouting(c.var.auth, deps.plans, body.attributes); + const { organizationId, mode } = c.var.auth; const found = await searchAvailability(c.var.auth, { eventTypeRef: body.event_type, fromMs: instant(body.start, "start"), @@ -1369,6 +1402,8 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA hostRefs: body.hosts, slotsPerHost: body.slots_per_host, limit: body.limit, + maxHosts: + mode === "live" ? await deps.plans?.limit(organizationId, "schedulingSearchHosts") : null, }); return c.json( { @@ -1404,6 +1439,7 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA router.openapi( createBookingRoute, async (c) => { + await requireRouting(c.var.auth, deps.plans, c.req.valid("json").attributes); const { booking, replayed } = await createBooking( c.var.auth, bookingRequest(c.req.valid("json"), c.req.valid("header")["Idempotency-Key"]), @@ -1543,6 +1579,7 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA router.openapi( createSeriesRoute, async (c) => { + await requireRouting(c.var.auth, deps.plans, c.req.valid("json").attributes); const { result, replayed } = await createSeries( c.var.auth, seriesRequest(c.req.valid("json"), c.req.valid("header")["Idempotency-Key"]), @@ -1566,6 +1603,7 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA ); router.openapi(previewSeriesRoute, async (c) => { + await requireRouting(c.var.auth, deps.plans, c.req.valid("json").attributes); const data = await previewSeries( c.var.auth, seriesRequest(c.req.valid("json"), undefined), @@ -1717,12 +1755,14 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA readOnly: body.read_only, apiKeyId: c.var.auth.apiKeyId, publicUrl: deps.publicUrl ?? null, + plans: deps.plans, }); return c.json(sessionBody(session), 201); }); router.openapi(createBookerSessionRoute, async (c) => { const body = c.req.valid("json"); + await requireRouting(c.var.auth, deps.plans, body.attributes); const session = await createBookerSession( c.var.auth, { diff --git a/apps/api/src/routes/sign.ts b/apps/api/src/routes/sign.ts index 7049c341e..1ea37c9ab 100644 --- a/apps/api/src/routes/sign.ts +++ b/apps/api/src/routes/sign.ts @@ -92,6 +92,10 @@ const documentNotFound = errorDoc( const requestNotFound = errorDoc( "No signature request with this id exists in the API key's environment (`not_found`).", ); +const heldBilling = errorDoc( + "In live environments: an overdue invoice or spend limit (`billing_hold`), or the project's budget " + + "(`budget_exceeded`).", +); const unavailable = errorDoc("Sign is temporarily unavailable (`sign_unavailable`)."); export const uploadSignDocumentRoute = createRoute({ @@ -213,6 +217,7 @@ export const createSignRequestRoute = createRoute({ }, }, responses: { + 402: heldBilling, 201: { description: "The signature request (`sent`, or `draft`). A replay returns the original request in its current state.", @@ -287,6 +292,7 @@ export const sendSignRequestRoute = createRoute({ middleware: [requireApiKey({ sign: ["send"] }), rateLimit("api.write")], request: { params: RequestIdParams }, responses: { + 402: heldBilling, 200: jsonDoc("The sent request.", SignRequestObject), ...commonErrors, 404: requestNotFound, diff --git a/apps/api/test/scheduling-plans.test.ts b/apps/api/test/scheduling-plans.test.ts new file mode 100644 index 000000000..d856f67fa --- /dev/null +++ b/apps/api/test/scheduling-plans.test.ts @@ -0,0 +1,431 @@ +import { describe, expect, test } from "bun:test"; +import { createPlanGate, createUsageGate, placeHold } from "@coss/billing"; +import { createTestContract } from "@coss/billing/testing"; +import { db, pool } from "@coss/db"; +import { createForm, type FormEnvironment } from "@coss/forms"; +import { CreateFormRequest } from "@coss/forms/schemas"; +import { testFormChecks } from "@coss/forms/testing"; +import { memoryLimiter } from "@coss/ratelimit/testing"; +import { chargeDueHostFees } from "@coss/scheduling"; +import { createTestTenant, type TestTenant } from "@coss/tenancy/testing"; +import { apiRequest, createTestApp, errorOf, type TestApp } from "./helpers.ts"; + +const MONDAY = "2031-01-06"; +const at = (time: string) => `${MONDAY}T${time}:00.000Z`; +const FORMS_URL = "http://forms.coss.test"; + +function call(app: TestApp, key: string, method: string, path: string, body?: unknown) { + return Promise.resolve( + app.request(`/v1/scheduling${path}`, { + method, + headers: { + Authorization: `Bearer ${key}`, + ...(body !== undefined ? { "Content-Type": "application/json" } : {}), + }, + ...(body !== undefined ? { body: JSON.stringify(body) } : {}), + }), + ); +} + +const hostBody = (uid: string) => ({ uid, name: `Host ${uid}`, time_zone: "UTC" }); + +const attendee = { uid: "patient_1", name: "Sam Lee", email: "sam@example.com", time_zone: "UTC" }; + +function enforced(extra: Parameters[0] = {}) { + return createTestApp({ plans: createPlanGate(), billing: createUsageGate(), ...extra }).app; +} + +async function onPlan(plan: "pro" | "scale"): Promise { + const tenant = await createTestTenant(); + await createTestContract( + tenant.organization.id, + { version: 1, plan }, + { effectiveFrom: new Date("2026-01-01T00:00:00Z") }, + ); + return tenant; +} + +/** A host with hours and a one-on-one event type, in the key's environment. */ +async function bookable(app: TestApp, key: string) { + expect((await call(app, key, "POST", "/hosts", hostBody("h1"))).status).toBe(201); + const schedule = await call(app, key, "POST", "/hosts/h1/schedules", { + rules: [{ days: [0, 1, 2, 3, 4, 5, 6], start: "09:00", end: "17:00" }], + }); + expect(schedule.status).toBe(201); + const created = await call(app, key, "POST", "/event_types", { + title: "Consultation", + duration_minutes: 30, + hosts: [{ host: "h1" }], + }); + expect(created.status).toBe(201); + return ((await created.json()) as { id: string }).id; +} + +describe("Scheduling's plan gates", () => { + test("Free has 3 live hosts across environments; sandbox has no limit", async () => { + const tenant = await createTestTenant(); + const app = enforced(); + for (const uid of ["a", "b", "c"]) { + expect((await call(app, tenant.liveKey, "POST", "/hosts", hostBody(uid))).status).toBe(201); + } + const refused = await call(app, tenant.liveKey, "POST", "/hosts", hostBody("d")); + expect(refused.status).toBe(402); + expect(await errorOf(refused)).toMatchObject({ + type: "billing_error", + code: "plan_limit_reached", + message: expect.stringContaining("3 scheduling hosts"), + }); + for (const uid of ["a", "b", "c", "d", "e"]) { + expect((await call(app, tenant.sandboxKey, "POST", "/hosts", hostBody(uid))).status).toBe( + 201, + ); + } + }); + + test("deleting a live host makes room for another", async () => { + const tenant = await createTestTenant(); + const app = enforced(); + for (const uid of ["a", "b", "c"]) { + await call(app, tenant.liveKey, "POST", "/hosts", hostBody(uid)); + } + expect((await call(app, tenant.liveKey, "DELETE", "/hosts/a")).status).toBe(200); + expect((await call(app, tenant.liveKey, "POST", "/hosts", hostBody("d"))).status).toBe(201); + }); + + test("Scale has no host limit past what it includes", async () => { + const tenant = await onPlan("scale"); + const app = enforced(); + for (const uid of ["a", "b", "c", "d"]) { + expect((await call(app, tenant.liveKey, "POST", "/hosts", hostBody(uid))).status).toBe(201); + } + }); + + test("the embedded host portal needs Scale", async () => { + const free = await createTestTenant(); + const app = enforced(); + await call(app, free.liveKey, "POST", "/hosts", hostBody("a")); + const refused = await call(app, free.liveKey, "POST", "/hosts/a/sessions", {}); + expect(refused.status).toBe(402); + expect(await errorOf(refused)).toMatchObject({ + code: "plan_feature_unavailable", + message: expect.stringContaining("Scale"), + }); + + await call(app, free.sandboxKey, "POST", "/hosts", hostBody("a")); + expect((await call(app, free.sandboxKey, "POST", "/hosts/a/sessions", {})).status).toBe(201); + + const scale = await onPlan("scale"); + await call(app, scale.liveKey, "POST", "/hosts", hostBody("a")); + expect((await call(app, scale.liveKey, "POST", "/hosts/a/sessions", {})).status).toBe(201); + }); + + test("holds need a paid plan; what they hold still confirms", async () => { + const free = await createTestTenant(); + const app = enforced(); + const eventType = await bookable(app, free.liveKey); + const refused = await call(app, free.liveKey, "POST", "/holds", { + event_type: eventType, + start: at("09:00"), + attendee, + }); + expect(refused.status).toBe(402); + expect((await errorOf(refused)).message).toContain("Pro"); + + const pro = await onPlan("pro"); + const proEvent = await bookable(app, pro.liveKey); + const held = await call(app, pro.liveKey, "POST", "/holds", { + event_type: proEvent, + start: at("09:00"), + attendee, + }); + expect(held.status).toBe(201); + }); + + test("attribute routing needs Scale, in live environments", async () => { + const pro = await onPlan("pro"); + const app = enforced(); + const eventType = await bookable(app, pro.liveKey); + const filtered = await call(app, pro.liveKey, "POST", "/event_types", { + title: "Pool", + kind: "pool", + duration_minutes: 30, + hosts: [], + host_filter: { specialty: "dermatology" }, + }); + expect(filtered.status).toBe(402); + const booked = await call(app, pro.liveKey, "POST", "/bookings", { + event_type: eventType, + start: at("09:00"), + attendee, + attributes: { specialty: "dermatology" }, + }); + expect(booked.status).toBe(402); + expect((await errorOf(booked)).code).toBe("plan_feature_unavailable"); + const plain = await call(app, pro.liveKey, "POST", "/bookings", { + event_type: eventType, + start: at("09:00"), + attendee, + }); + expect(plain.status).toBe(201); + }); + + test("a group session's seats are limited by plan, and an existing size can stay", async () => { + const free = await createTestTenant(); + const app = enforced(); + await call(app, free.liveKey, "POST", "/hosts", hostBody("a")); + const group = (capacity: number) => ({ + title: "Class", + kind: "group", + capacity, + duration_minutes: 60, + hosts: [{ host: "a" }], + }); + const refused = await call(app, free.liveKey, "POST", "/event_types", group(11)); + expect(refused.status).toBe(402); + expect((await errorOf(refused)).message).toContain("10 seats per group session"); + const created = await call(app, free.liveKey, "POST", "/event_types", group(10)); + expect(created.status).toBe(201); + const { id } = (await created.json()) as { id: string }; + expect( + (await call(app, free.liveKey, "PATCH", `/event_types/${id}`, { capacity: 11 })).status, + ).toBe(402); + expect( + (await call(app, free.liveKey, "PATCH", `/event_types/${id}`, { capacity: 8 })).status, + ).toBe(200); + }); + + test("a live search covers the plan's hosts and says there were more", async () => { + const pro = await onPlan("pro"); + const app = enforced(); + const uids = Array.from({ length: 3 }, (_, index) => `h${index}`); + for (const uid of uids) { + await call(app, pro.liveKey, "POST", "/hosts", hostBody(uid)); + await call(app, pro.liveKey, "POST", `/hosts/${uid}/schedules`, { + rules: [{ days: [0, 1, 2, 3, 4, 5, 6], start: "09:00", end: "17:00" }], + }); + } + const created = await call(app, pro.liveKey, "POST", "/event_types", { + title: "Pool", + kind: "pool", + duration_minutes: 30, + hosts: uids.map((host) => ({ host })), + }); + const { id } = (await created.json()) as { id: string }; + const search = async () => { + const res = await call(app, pro.liveKey, "POST", "/availability/search", { + event_type: id, + start: at("09:00"), + end: at("12:00"), + }); + expect(res.status).toBe(200); + return (await res.json()) as { data: unknown[]; has_more: boolean }; + }; + expect(await search()).toMatchObject({ has_more: false }); + expect((await search()).data).toHaveLength(3); + + // The same search with the plan's limit below the pool. + const small = enforced({ plans: { ...createPlanGate(), limit: async () => 2 } }); + const res = await call(small, pro.liveKey, "POST", "/availability/search", { + event_type: id, + start: at("09:00"), + end: at("12:00"), + }); + const cut = (await res.json()) as { data: unknown[]; has_more: boolean }; + expect(cut.data).toHaveLength(2); + expect(cut.has_more).toBe(true); + }); + + test("live slot lists are limited per organization at the plan's rate", async () => { + const tenant = await createTestTenant(); + const app = enforced({ rateLimiter: memoryLimiter() }); + const eventType = await bookable(app, tenant.liveKey); + const path = `/event_types/${eventType}/slots?start=${at("09:00")}&end=${at("10:00")}`; + let status = 200; + for (let index = 0; index < 60 && status === 200; index++) { + status = (await call(app, tenant.liveKey, "GET", path)).status; + } + expect(status).toBe(429); + }); + + test("pages show the attribution below Scale, and not on it", async () => { + const app = enforced(); + const sessionOf = async (tenant: TestTenant) => { + const eventType = await bookable(app, tenant.liveKey); + const res = await call(app, tenant.liveKey, "POST", "/booker_sessions", { + event_type: eventType, + }); + const { token } = (await res.json()) as { token: string }; + const session = await app.request("/scheduling/client/session", { + headers: { Authorization: `Bearer ${token}` }, + }); + return (await session.json()) as { coss_attribution: boolean }; + }; + expect((await sessionOf(await createTestTenant())).coss_attribution).toBe(true); + expect((await sessionOf(await onPlan("scale"))).coss_attribution).toBe(false); + }); +}); + +describe("the host fee", () => { + const usage = async (organizationId: string) => { + const { rows } = await pool.query<{ quantity: string }>( + "select coalesce(sum(quantity), 0) as quantity from billing_usage where organization_id = $1 and meter = 'scheduling.hosts'", + [organizationId], + ); + return Number(rows[0]?.quantity); + }; + + test("nothing is charged before the meter is priced, and a day at a time after", async () => { + const tenant = await onPlan("scale"); + const app = enforced(); + await call(app, tenant.liveKey, "POST", "/hosts", hostBody("a")); + await call(app, tenant.sandboxKey, "POST", "/hosts", hostBody("sandbox")); + const scope = { organizationIds: [tenant.organization.id] }; + expect(await chargeDueHostFees({ now: new Date("2026-12-31T23:00:00Z"), scope })).toBe(0); + expect(await usage(tenant.organization.id)).toBe(0); + // The first day of the priced card, once; the sandbox host is never charged. + expect(await chargeDueHostFees({ now: new Date("2027-01-01T00:00:01Z"), scope })).toBe(1); + expect(await chargeDueHostFees({ now: new Date("2027-01-01T00:00:02Z"), scope })).toBe(0); + expect(await chargeDueHostFees({ now: new Date("2027-01-02T00:00:01Z"), scope })).toBe(1); + expect(await usage(tenant.organization.id)).toBe(2); + }); +}); + +describe("holds reach every new live usage", () => { + const held = () => onPlan("scale"); + const hold = (tenant: TestTenant) => + db.transaction((tx) => placeHold(tx, tenant.organization.id, "delinquent", "Overdue")); + + test("new bookings and series are refused; holds already made still confirm", async () => { + const tenant = await held(); + const app = enforced(); + const eventType = await bookable(app, tenant.liveKey); + const earlier = await call(app, tenant.liveKey, "POST", "/holds", { + event_type: eventType, + start: at("10:00"), + attendee, + }); + expect(earlier.status).toBe(201); + const { id } = (await earlier.json()) as { id: string }; + await hold(tenant); + + const refused = await call(app, tenant.liveKey, "POST", "/bookings", { + event_type: eventType, + start: at("09:00"), + attendee, + }); + expect(refused.status).toBe(402); + expect((await errorOf(refused)).code).toBe("billing_hold"); + expect( + ( + await call(app, tenant.liveKey, "POST", "/holds", { + event_type: eventType, + start: at("11:00"), + attendee, + }) + ).status, + ).toBe(402); + expect((await call(app, tenant.liveKey, "POST", `/holds/${id}/confirm`, {})).status).toBe(200); + }); + + test("the person on the booking page doesn't hear about the customer's billing", async () => { + const tenant = await held(); + const app = enforced(); + const eventType = await bookable(app, tenant.liveKey); + const session = await call(app, tenant.liveKey, "POST", "/booker_sessions", { + event_type: eventType, + }); + const { token } = (await session.json()) as { token: string }; + await hold(tenant); + const res = await app.request("/scheduling/client/bookings", { + method: "POST", + headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" }, + body: JSON.stringify({ start: at("09:00"), attendee }), + }); + expect(res.status).toBe(403); + const error = await errorOf(res); + expect(error.code).toBe("account_suspended"); + expect(error.message).not.toMatch(/invoice|spend|budget/i); + }); + + test("a sandbox keeps booking", async () => { + const tenant = await held(); + const app = enforced(); + const eventType = await bookable(app, tenant.sandboxKey); + await hold(tenant); + expect( + ( + await call(app, tenant.sandboxKey, "POST", "/bookings", { + event_type: eventType, + start: at("09:00"), + attendee, + }) + ).status, + ).toBe(201); + }); + + test("hosted forms close to live submissions, and look closed", async () => { + const tenant = await held(); + const env: FormEnvironment = { + organizationId: tenant.organization.id, + projectId: tenant.project.id, + environmentId: tenant.production.id, + mode: "live", + }; + const form = await createForm(env, CreateFormRequest.parse({ name: "Contact" }), { + links: { publicUrl: FORMS_URL }, + checks: testFormChecks(), + }); + const { app } = createTestApp(); + const post = () => + app.fetch( + new Request(`${FORMS_URL}/f/${form.id}`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "text/html,application/xhtml+xml,*/*;q=0.8", + Origin: "https://www.example.com", + }, + body: new URLSearchParams({ email: "ada@example.com", message: "Hi" }).toString(), + }), + ); + expect((await post()).status).toBe(200); + await hold(tenant); + const refused = await post(); + expect(refused.status).toBe(404); + expect(await refused.text()).not.toMatch(/invoice|spend|billing/i); + }); + + test("new live inboxes are refused", async () => { + const tenant = await held(); + const { app } = createTestApp(); + await hold(tenant); + const refused = await apiRequest(app, "POST", "/v1/inboxes", { + key: tenant.liveKey, + body: { username: "support" }, + }); + expect(refused.status).toBe(402); + expect((await errorOf(refused)).code).toBe("billing_hold"); + }); + + test("new live signature requests are refused", async () => { + const tenant = await held(); + const { app } = createTestApp(); + await hold(tenant); + const refused = await apiRequest(app, "POST", "/v1/sign/requests", { + key: tenant.liveKey, + body: { + title: "NDA", + documents: [{ filename: "nda.pdf", content: Buffer.from("%PDF").toString("base64") }], + signers: [ + { + name: "Ada Lovelace", + email: "ada@example.com", + fields: [{ type: "signature", anchor: "{{signature:signer_1}}" }], + }, + ], + }, + }); + expect(refused.status).toBe(402); + expect((await errorOf(refused)).code).toBe("billing_hold"); + }); +}); diff --git a/apps/scheduling/AGENTS.md b/apps/scheduling/AGENTS.md index 2e436735f..b6a674881 100644 --- a/apps/scheduling/AGENTS.md +++ b/apps/scheduling/AGENTS.md @@ -14,7 +14,8 @@ would make it same-site with the dashboard). Product rules are in `packages/sche `packages/sdk/src/scheduling.ts`. - Times are UTC instants from the API, shown in the browser's zone. This app never works out what's available. - A page is the customer's: their logo or name (`BrandMark`) and accent (`useAccentColor`, which also picks the text - color), and nothing of coss.com's. The logo arrives as a data URL in `/session`, so the CSP stays free of other hosts. + color), and nothing of coss.com's but `CossAttribution` ("Scheduling by coss.com", a plain link below the page, on + plans without `schedulingWhiteLabel`: never an image or anything else that loads). The logo arrives as a data URL in `/session`, so the CSP stays free of other hosts. - The Google button (`continue-with-google.tsx`) follows Google's branding rules, and its OAuth app passed verification: keep the white fill, the `#747775` border, the `#1F1F1F` Google Sans Medium 14/20 text, the padding and the logo. Its height and corners match the buttons beside it, which the rules leave open. diff --git a/apps/scheduling/src/app/b/[sessionId]/booker.tsx b/apps/scheduling/src/app/b/[sessionId]/booker.tsx index 7d997233a..cb1bcce3f 100644 --- a/apps/scheduling/src/app/b/[sessionId]/booker.tsx +++ b/apps/scheduling/src/app/b/[sessionId]/booker.tsx @@ -28,6 +28,7 @@ import { type FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { BrandMark, BrokenLink, + CossAttribution, Detail, DetailList, LoadingPanel, @@ -220,6 +221,7 @@ function Confirmed(props: { session: Session; done: Done; when: string; timeZone + ); } @@ -508,6 +510,7 @@ export function Booker({ sessionId, apiUrl }: { sessionId: string; apiUrl: strin )} + ); } diff --git a/apps/scheduling/src/app/h/[sessionId]/host-portal.tsx b/apps/scheduling/src/app/h/[sessionId]/host-portal.tsx index d72f8ee79..efa285b01 100644 --- a/apps/scheduling/src/app/h/[sessionId]/host-portal.tsx +++ b/apps/scheduling/src/app/h/[sessionId]/host-portal.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from "react"; import { BrandMark, BrokenLink, + CossAttribution, LoadingPanel, PageTitle, Panel, @@ -75,6 +76,7 @@ export function HostPortal({ sessionId, apiUrl }: { sessionId: string; apiUrl: s + ); } diff --git a/apps/scheduling/src/app/m/[sessionId]/manage.tsx b/apps/scheduling/src/app/m/[sessionId]/manage.tsx index 4b6b9915a..471e5a348 100644 --- a/apps/scheduling/src/app/m/[sessionId]/manage.tsx +++ b/apps/scheduling/src/app/m/[sessionId]/manage.tsx @@ -8,6 +8,7 @@ import { useEffect, useMemo, useState } from "react"; import { BrandMark, BrokenLink, + CossAttribution, Detail, DetailList, LoadingPanel, @@ -201,6 +202,7 @@ export function Manage({ sessionId, apiUrl }: { sessionId: string; apiUrl: strin ) : null} + ); } diff --git a/apps/scheduling/src/components/page.tsx b/apps/scheduling/src/components/page.tsx index 40c38ff9e..3bab9a277 100644 --- a/apps/scheduling/src/components/page.tsx +++ b/apps/scheduling/src/components/page.tsx @@ -8,7 +8,24 @@ import type { Branding } from "@/lib/branding"; const LOGO_MAX_WIDTH = 180; const LOGO_MAX_HEIGHT = 40; -/** The customer's logo, else their name; nothing of coss.com's on a page that's theirs. */ +/** "Scheduling by coss.com", below the page, on plans without the white label. */ +export function CossAttribution({ branding }: { branding: Branding | null | undefined }) { + if (!branding?.coss_attribution) return null; + return ( +

+ + Scheduling by coss.com + +

+ ); +} + +/** The customer's logo, else their name; the attribution above is the only mark of coss.com's. */ export function BrandMark({ branding }: { branding: Branding }) { const { logo, brand_name: name } = branding; if (logo) { diff --git a/apps/scheduling/src/lib/branding.ts b/apps/scheduling/src/lib/branding.ts index 6203049ef..0e971935a 100644 --- a/apps/scheduling/src/lib/branding.ts +++ b/apps/scheduling/src/lib/branding.ts @@ -7,6 +7,8 @@ export type Branding = { brand_name: string | null; accent_color: string | null; logo: Logo | null; + /** The plan shows "Scheduling by coss.com" on its pages. */ + coss_attribution?: boolean; }; /** The customer's accent color, with text that reads on it. A white page would hide a pale one. */ diff --git a/apps/scheduling/test/attribution.test.tsx b/apps/scheduling/test/attribution.test.tsx new file mode 100644 index 000000000..41187d701 --- /dev/null +++ b/apps/scheduling/test/attribution.test.tsx @@ -0,0 +1,22 @@ +import { describe, expect, test } from "bun:test"; +import { renderToStaticMarkup } from "react-dom/server"; +import { CossAttribution } from "../src/components/page.tsx"; + +const branding = { brand_name: "Acme", accent_color: null, logo: null }; + +describe("CossAttribution", () => { + test("shows on plans without the white label, as a link that sends no referrer", () => { + const html = renderToStaticMarkup( + , + ); + expect(html).toContain("Scheduling by coss.com"); + expect(html).toContain('rel="noopener noreferrer"'); + expect(html).not.toContain(" { + for (const value of [{ ...branding, coss_attribution: false }, branding, null, undefined]) { + expect(renderToStaticMarkup()).toBe(""); + } + }); +}); diff --git a/apps/site/content/docs/(platform)/billing.mdx b/apps/site/content/docs/(platform)/billing.mdx index c4fe98690..4a711f0ef 100644 --- a/apps/site/content/docs/(platform)/billing.mdx +++ b/apps/site/content/docs/(platform)/billing.mdx @@ -72,8 +72,10 @@ after removing it. On a contract, add-ons are part of the contract. its inbox-days. Received mail past its allowance is dropped (and counted on **Received emails**). - If an invoice can't be collected, we retry it and email your billing contacts. Once it's overdue past the grace period (14 days unless your contract says otherwise), live sending is - paused (`402`, `billing_hold`) until it's paid. Sandbox keys keep working, and scheduled emails - wait rather than fail. + paused (`402`, `billing_hold`) until it's paid. So are new live bookings, signature requests and + inboxes, and hosted forms stop accepting submissions. Sandbox keys keep working, scheduled emails + wait rather than fail, and bookings already made stay. Mail still arrives, and monthly charges for + inboxes, hosts and phone numbers you keep continue. - Owners can set a **spend limit**: when usage beyond your credit reaches it, live sending pauses until the next month or until the limit is raised. Usage alerts go out at 80% and 100% of your credit (owners can change the thresholds). diff --git a/apps/site/content/docs/(platform)/hipaa.mdx b/apps/site/content/docs/(platform)/hipaa.mdx index c5b0fe302..941f94e53 100644 --- a/apps/site/content/docs/(platform)/hipaa.mdx +++ b/apps/site/content/docs/(platform)/hipaa.mdx @@ -15,13 +15,13 @@ Don't send PHI to coss.com without a signed Business Associate Agreement. Our ## Turning it on -You can't turn HIPAA mode on yourself, by any key or role. Ask for it from **Settings → Compliance** +HIPAA mode is contract-only: it comes with an Enterprise contract and isn't available on self-serve +plans. You can't turn it on yourself, by any key or role. Ask for it from **Settings → Compliance** in the dashboard. Then: 1. **You sign a Business Associate Agreement (BAA)** with coss.com, and the mode becomes a line on your contract. -2. **Your plan must include the audit log**, as the plans do, or - your contract must grant it. +2. **Your contract must include the audit log**, as Enterprise does. 3. **coss.com staff turn it on** and record the BAA's date and reference. It applies at once, including to messages still waiting to go out. diff --git a/apps/site/content/docs/(platform)/spend.mdx b/apps/site/content/docs/(platform)/spend.mdx index 73f1f3159..2c4b8f761 100644 --- a/apps/site/content/docs/(platform)/spend.mdx +++ b/apps/site/content/docs/(platform)/spend.mdx @@ -66,7 +66,10 @@ you change them) it emails the billing contacts, and the member it concerns, and start of the next month, or as soon as an owner raises (or removes) the budget. Checks run every few minutes, so spend can pass a budget by what is sent in between. Emails -already accepted before the pause still go out. A budget of $0 allows no billable usage at all. +already accepted before the pause still go out. New live bookings, signature requests and inboxes +in a project over budget are refused too, and its hosted forms stop accepting submissions. Mail +still arrives and monthly charges for what you keep continue, so even a budget of $0 can't stop +those. Budgets sit alongside your organization's **spend limit** (in billing settings), which pauses all live sending once usage beyond your credit reaches it. diff --git a/apps/site/content/docs/scheduling/bookings.mdx b/apps/site/content/docs/scheduling/bookings.mdx index 489179564..d4ddf4f41 100644 --- a/apps/site/content/docs/scheduling/bookings.mdx +++ b/apps/site/content/docs/scheduling/bookings.mdx @@ -23,7 +23,7 @@ book twice. A refused request keeps no key. ## Taking payment first -Hold the slot, charge the customer, then confirm. A hold isn't charged and tells nobody. Its slot stays +Hold the slot, charge the customer, then confirm. Live holds need . A hold isn't charged and tells nobody. Its slot stays yours until it expires, though a plan's usage limit can still refuse the confirmation. ```ts twoslash diff --git a/apps/site/content/docs/scheduling/embedding.mdx b/apps/site/content/docs/scheduling/embedding.mdx index 5a43046fc..7fac0e998 100644 --- a/apps/site/content/docs/scheduling/embedding.mdx +++ b/apps/site/content/docs/scheduling/embedding.mdx @@ -12,6 +12,10 @@ thing, with its token in the URL's fragment, which never reaches a server. | `createForBooker` | Book one event type | 30 minutes by default, up to a day | | `createForBooking` | Reschedule or cancel one booking | Until a day after it ends. Stops when it's cancelled | +In live environments, `createForHost` needs . On other plans, connect +a host's calendars and set their hours through the API. Pages show "Scheduling by coss.com" below them, except on +. + ```ts twoslash import { Coss } from "coss"; const coss = new Coss(); diff --git a/apps/site/content/docs/scheduling/hosts.mdx b/apps/site/content/docs/scheduling/hosts.mdx index e799dcb20..28558d4db 100644 --- a/apps/site/content/docs/scheduling/hosts.mdx +++ b/apps/site/content/docs/scheduling/hosts.mdx @@ -18,6 +18,10 @@ await coss.scheduling.hosts.create({ }); ``` +Your plan sets how many live hosts your organization can have: on +Free and on Pro. Scale includes +and charges for more. Delete a host to stop counting them. + An `email` is where their notifications go, not an identity: two hosts can share one, and a host needs none. `attributes` are what [pools](/docs/scheduling/pools-and-groups) choose hosts by. diff --git a/apps/site/content/docs/scheduling/pools-and-groups.mdx b/apps/site/content/docs/scheduling/pools-and-groups.mdx index be3b64a57..0b04cf35b 100644 --- a/apps/site/content/docs/scheduling/pools-and-groups.mdx +++ b/apps/site/content/docs/scheduling/pools-and-groups.mdx @@ -44,6 +44,9 @@ See [Routing](/docs/scheduling/routing) for sending each person to the right kin ## Who's free? For a marketplace's search page: each matching host's soonest slots, soonest first, across up to 500 hosts. +In live environments your plan sets how many hosts one search covers +( on Free, on Pro, + on Scale). `has_more` is `true` when there were more than it looked at. ```ts twoslash import { Coss } from "coss"; @@ -68,7 +71,9 @@ interpreter. Add at most one `member`, and one of a pool is chosen alongside the With `kind: "group"` and a `capacity`, each attendee books a seat in the same session, with a booking and manage link of their own. The host's time is taken once, by the first seat, and given back with the last. -A full session answers `slot_unavailable`. A seat moves by cancelling it and booking another session. +A full session answers `slot_unavailable`. A seat moves by cancelling it and booking another session. In +live environments your plan sets the largest `capacity`: on +Free, on Pro and on Scale. A session is one meeting. The first seat's `location` is everyone's, so don't pass one attendee's own details in it: a `phone` location only ever shows the number on the event type, and a booking page's seat can't fill one in. diff --git a/apps/site/content/docs/scheduling/routing.mdx b/apps/site/content/docs/scheduling/routing.mdx index fd2f99a5d..8fe495470 100644 --- a/apps/site/content/docs/scheduling/routing.mdx +++ b/apps/site/content/docs/scheduling/routing.mdx @@ -4,7 +4,8 @@ description: Send each person to the right host, such as a specialist or someone --- Describe your hosts with attributes, then ask for what each person needs. Coss.com only offers times, and only -books, among the hosts who match. +books, among the hosts who match. In live environments, `host_filter` and `attributes` need +; sandbox environments have them on every plan. ## Describe your hosts diff --git a/apps/site/content/products/scheduling.mdx b/apps/site/content/products/scheduling.mdx index aa82c0bf1..772469740 100644 --- a/apps/site/content/products/scheduling.mdx +++ b/apps/site/content/products/scheduling.mdx @@ -170,9 +170,9 @@ Our [Terms](/legal/terms) don't allow special categories of personal data or pro 10 minutes by default, 1 minute to 24 hours 1 day by default, up to 30 days up to 52 occurrences, weekly or monthly -2 to 10,000 seats +2 to 10,000 seats, by plan up to 62 days -up to 500 hosts +up to 500 hosts, by plan 400 days 25 1 hour by default, at most 24 hours @@ -184,14 +184,29 @@ Our [Terms](/legal/terms) don't allow special categories of personal data or pro -
+
- + The Free plan includes . A group session is one booking however many attend, and each occurrence of a series is one. Rescheduling is free, and a cancelled booking stays charged. Booking emails sent from your own domain are billed like any email, and SMS reminders like any text message. See [pricing](/pricing) for the plans.
+
+ +- **Hosts**: on Free and on Pro. Scale includes and charges for more by the day. +- **Holds** for taking payment first: . +- **The embedded host portal**: . On other plans, set hours and connect calendars through the API. +- **Attribute routing** with `host_filter` and `attributes`: . +- **Seats per group session**: on Free, on Pro and on Scale. +- **Hosts per availability search**: on Free, on Pro and on Scale. +- **Hosted pages without "Scheduling by coss.com"**: . +- **Patient data**: a BAA and [HIPAA mode](/docs/hipaa), by Enterprise contract. + +What you already set up keeps working if you change plans. See [compare plans](/pricing#compare). + +
+
With a sandbox (`coss_test_…`) key, emails go to the fake provider, so nobody's mailed, and the dashboard shows them with their links. Connect a simulated calendar and set its busy times with `coss.scheduling.calendars.setSimulatedBusy`, and test your handlers with `coss trigger scheduling.booking.confirmed`. See [testing](/docs/scheduling/testing). diff --git a/apps/site/content/products/security.mdx b/apps/site/content/products/security.mdx index e69b69647..1684cbf4d 100644 --- a/apps/site/content/products/security.mdx +++ b/apps/site/content/products/security.mdx @@ -15,6 +15,7 @@ features: - An audit log of every change - Reputation guard and content screening - A published data retention policy + - HIPAA mode with a signed BAA, by contract related: - platform - risk @@ -97,6 +98,20 @@ Every kind of data has a published retention period, enforced by the worker's pu
+
+ +HIPAA mode comes with an Enterprise contract and is turned on by coss.com once the BAA is signed. It isn't available on self-serve plans. The safeguards support your HIPAA compliance, which also depends on how you use coss.com, and the Terms don't allow protected health information without a signed agreement. + +- **No AI on your content**: messages are never sampled or sent to a model, and forms, Risk and Scheduling skip content scores. +- **Less kept**: email content is deleted once each email is finished, SMS bodies aren't kept, tracking is off and request logs keep no bodies. +- **Tighter access**: two-factor is required, idle dashboard sessions end after 15 minutes, and coss.com staff can't view content or impersonate your members. +- **Encrypted delivery**: email goes out only over TLS. A message is dropped rather than delivered unencrypted. +- **What's covered**: **Settings → Compliance** shows the mode's status, your BAA and each vendor's BAA status. + +See [HIPAA mode](/docs/hipaa) for every control, what it refuses and what stays up to you. + +
+
The [Data Processing Addendum](/legal/dpa) is part of the [Terms](/legal/terms): it covers the security measures above, the subprocessors and their locations, and transfers under the EU-U.S. Data Privacy Framework and the Standard Contractual Clauses. The [Privacy Policy](/legal/privacy) and the Acceptable Use Policy say what coss.com collects and what it allows. Vulnerabilities can be reported through the responsible disclosure program at [cal.com/security](https://cal.com/security). diff --git a/apps/site/content/solutions/booking.mdx b/apps/site/content/solutions/booking.mdx index fd77366e1..c908f408f 100644 --- a/apps/site/content/solutions/booking.mdx +++ b/apps/site/content/solutions/booking.mdx @@ -191,8 +191,8 @@ Bookings and calendars send [webhooks](/docs/webhooks) with ids, times and statu
- + -The Free plan includes . Booking emails sent from your own domain are billed like any email and SMS reminders like any text message, and live SMS needs a paid plan. Sandbox environments are free. See [pricing](/pricing) for the plans. +The Free plan includes and hosts, and Scale includes . A marketplace's search, attribute routing and embedded host portal need . Booking emails sent from your own domain are billed like any email and SMS reminders like any text message, and live SMS needs a paid plan. Sandbox environments are free. See [pricing](/pricing) for the plans.
diff --git a/apps/site/src/app/(home)/pricing/page.tsx b/apps/site/src/app/(home)/pricing/page.tsx index 997323a98..8b75e015f 100644 --- a/apps/site/src/app/(home)/pricing/page.tsx +++ b/apps/site/src/app/(home)/pricing/page.tsx @@ -115,7 +115,8 @@ export default function PricingPage() {

{PLANS.enterprise.name}

Annual contracts from {formatPrice(PLANS.enterprise.feeCents)} a month: volume rates - and committed spend, directory sync, invoicing with payment terms and direct debit. + and committed spend, directory sync, HIPAA mode with a signed BAA, invoicing with + payment terms and direct debit.

diff --git a/apps/site/src/components/pricing/pricing-faq.tsx b/apps/site/src/components/pricing/pricing-faq.tsx index 2f8264659..4287f2ef8 100644 --- a/apps/site/src/components/pricing/pricing-faq.tsx +++ b/apps/site/src/components/pricing/pricing-faq.tsx @@ -61,6 +61,21 @@ function questions(): { question: string; answer: ReactNode; text?: string }[] { answer: "Yes. Owners can set a spend limit: when usage beyond your credit reaches it, live sending pauses until the next month or until the limit is raised. Usage alerts go out at 80% and 100% of your credit, and owners can change those thresholds.", }, + { + question: "Do you sign a BAA?", + answer: ( + <> + Yes, on an {PLANS.enterprise.name} contract: coss.com signs a Business Associate Agreement + and turns on{" "} + + HIPAA mode + + , which narrows where protected health information can go and who at coss.com can reach + it. It isn't available on self-serve plans. + + ), + text: `Yes, on an ${PLANS.enterprise.name} contract: coss.com signs a Business Associate Agreement and turns on HIPAA mode, which narrows where protected health information can go and who at coss.com can reach it. It isn't available on self-serve plans.`, + }, { question: "Can I send SMS?", answer: ( diff --git a/apps/site/src/lib/billing-text.ts b/apps/site/src/lib/billing-text.ts index a8d201355..23cad13bf 100644 --- a/apps/site/src/lib/billing-text.ts +++ b/apps/site/src/lib/billing-text.ts @@ -25,11 +25,14 @@ import { SUPPORT_TIERS, } from "@coss/billing/catalog"; import { formatCents, formatUnitPrice } from "@coss/billing/money"; -import { emailTierCharges, formatPrice, METER_NOTES, UNRELEASED_METERS } from "./pricing"; +import { emailTierCharges, formatPrice, METER_NOTES } from "./pricing"; /** "3,000 email recipients a month"; inbox-days read as the inboxes they cover. */ function includedText(meter: MeterKey, n: number): string { if (meter === "email.inboxes") return `${Math.floor(n / 31).toLocaleString("en-US")} inboxes`; + if (meter === "scheduling.hosts") { + return `${Math.floor(n / 31).toLocaleString("en-US")} Scheduling hosts`; + } if (meter === "email.received") return `${n.toLocaleString("en-US")} received emails a month`; return `${meterAmount(meter, n)} a month`; } @@ -138,6 +141,14 @@ export function planRows(): { label: string; values: string[] }[] { return `${limit.toLocaleString("en-US")} marketing emails a second`; }), }, + { + label: "Scheduling slot rate", + values: plans.map((plan) => { + if (plan.key === "enterprise") return "Custom"; + const { limit } = plan.entitlements.rateLimits["scheduling.slots.second"]; + return `${limit.toLocaleString("en-US")} slot lists or searches a second`; + }), + }, ...LIMIT_KEYS.map((key) => ({ label: LIMIT_LABELS[key], values: plans.map((plan) => @@ -156,6 +167,10 @@ export function planRows(): { label: string; values: string[] }[] { : "—", ), })), + { + label: "HIPAA mode with a BAA", + values: plans.map((plan) => (plan.key === "enterprise" ? "By contract" : "—")), + }, ]; } @@ -190,7 +205,6 @@ export function rateRows( const card = RATE_CARDS.at(-1); if (!card) return []; return METER_KEYS.flatMap((meter) => { - if (UNRELEASED_METERS.has(meter)) return []; const definition = METERS[meter]; if (typeof only === "string" && definition.product !== only) return []; if (Array.isArray(only) && !only.includes(meter)) return []; diff --git a/apps/site/src/lib/compare-data/trust.ts b/apps/site/src/lib/compare-data/trust.ts index 22a9cffad..940ddfca4 100644 --- a/apps/site/src/lib/compare-data/trust.ts +++ b/apps/site/src/lib/compare-data/trust.ts @@ -1,6 +1,6 @@ import { BUCKETS } from "@coss/ratelimit/buckets"; import { formatRetentionDays, RETENTION_DAYS } from "@coss/retention"; -import { includedUnitsText, planPriceText, rateText } from "../billing-text"; +import { includedUnitsText, limitText, planPriceText, rateText } from "../billing-text"; import type { Competitor } from "../compare"; import { formatRule } from "../rate-limits-text"; @@ -20,18 +20,18 @@ const NYLAS_SCHEDULER: Competitor = { products: ["scheduling"], query: "Nylas Scheduler alternative", verdict: - "Nylas Scheduler has public booking links, Stripe payments and a BAA. coss.com Scheduling bills per booking, on the key you use for email and SMS.", - lede: "Both give your product hosts, availability, calendar connections, one-on-one, collective, round-robin and group meetings, and a page or component to book on. Nylas Scheduler sits on Nylas's email and calendar APIs, is priced per connected account, and adds public links, native Stripe payments and a BAA. coss.com Scheduling is priced per booking, keeps only busy times from calendars, and shares its API key, webhooks and logs with the rest of coss.com.", + "Nylas Scheduler has public booking links and Stripe payments. coss.com Scheduling bills per booking, on the key you use for email and SMS.", + lede: "Both give your product hosts, availability, calendar connections, one-on-one, collective, round-robin and group meetings, and a page or component to book on. Nylas Scheduler sits on Nylas's email and calendar APIs, is priced per connected account, and adds public links and native Stripe payments. coss.com Scheduling is priced per booking, with a monthly charge for hosts beyond your plan's, keeps only busy times from calendars, and shares its API key, webhooks and logs with the rest of coss.com.", checked: CHECKED, rows: [ { label: "Price", - coss: `${rateText("scheduling.bookings")}, charged when confirmed; nothing per host or calendar`, + coss: `${rateText("scheduling.bookings")}, charged when confirmed. Scale includes ${includedUnitsText("scheduling.hosts", "scale")}, then ${rateText("scheduling.hosts")}`, them: "No per-booking price. Per connected account: Essentials $15 a month with 10 calendar-only accounts, then $1.70 each; Pro $49 a month with 35, then $1.50 each", }, { label: "Free", - coss: includedUnitsText("scheduling.bookings"), + coss: `${includedUnitsText("scheduling.bookings")} and ${limitText("schedulingHosts", "free")} hosts`, them: "5 email and calendar accounts; calendar-only accounts aren't included", }, { @@ -46,9 +46,14 @@ const NYLAS_SCHEDULER: Competitor = { }, { label: "Calendar consent", - coss: "Hosts consent to coss.com's Google and Microsoft apps. Using your own apps isn't offered", + coss: "Hosts consent to coss.com's Google and Microsoft apps today. Your own apps are coming soon to Enterprise", them: "Your own Google and Microsoft apps, or Nylas's shared Google app with the Accelerator add-on ($2,400 a year, on Pro Annual)", }, + { + label: "Booking domain", + coss: "Hosted pages are on coss.com's booking domain today. Your own domain is coming soon to Enterprise", + them: "Public pages at book.nylas.com, or its components on your own site", + }, { label: "Meeting kinds", coss: "One-on-one, pool (shared by weight), collective, group (2 to 10,000 seats)", @@ -83,7 +88,7 @@ const NYLAS_SCHEDULER: Competitor = { }, { label: "Finding a host", - coss: "Host attributes narrow a pool per booking; availability.search covers up to 500 hosts", + coss: "On Scale and Enterprise, host attributes narrow a pool per booking and availability.search covers up to 500 hosts", them: "—", note: "Not compared: see Nylas's docs", }, @@ -109,7 +114,7 @@ const NYLAS_SCHEDULER: Competitor = { }, { label: "HIPAA", - coss: "No BAA yet; the Terms don't allow protected health information without a written agreement", + coss: "A signed BAA and HIPAA mode, by Enterprise contract", them: "Pre-signed BAA on every paid plan", }, { @@ -134,8 +139,8 @@ const NYLAS_SCHEDULER: Competitor = { body: "Nylas has native Stripe payments, with the booking finalized once payment succeeds. coss.com has no built-in payments: you hold the slot and charge through your own provider.", }, { - title: "You handle patient data", - body: "Nylas offers a pre-signed BAA on every paid plan. coss.com doesn't offer one yet, and its Terms don't allow protected health information without a written agreement.", + title: "You need a BAA without a contract", + body: "Nylas offers a pre-signed BAA on every paid plan. coss.com signs a BAA and turns on HIPAA mode on an Enterprise contract, and its Terms don't allow protected health information without one.", }, { title: "You need more of the calendar than free and busy", @@ -202,7 +207,7 @@ const NYLAS_SCHEDULER: Competitor = { }, { question: "What does coss.com Scheduling cost next to Nylas?", - answer: `coss.com charges ${rateText("scheduling.bookings")} once it's confirmed, with ${includedUnitsText("scheduling.bookings")} on Free. Beyond that you need a paid plan, such as Pro at ${planPriceText("pro")}, whose fee counts toward usage. Nylas charges per connected account and lists no per-booking price, so which costs less depends on how many bookings each host takes.`, + answer: `coss.com charges ${rateText("scheduling.bookings")} once it's confirmed, with ${includedUnitsText("scheduling.bookings")} on Free. Beyond that you need a paid plan, such as Pro at ${planPriceText("pro")}, whose fee counts toward usage. Scale includes ${includedUnitsText("scheduling.hosts", "scale")}, then ${rateText("scheduling.hosts")}. Nylas charges per connected account and lists no per-booking price, so which costs less depends on how many bookings each host takes.`, }, { question: "Can coss.com take payment for a booking?", @@ -212,7 +217,7 @@ const NYLAS_SCHEDULER: Competitor = { { question: "Will coss.com sign a BAA for Scheduling?", answer: - "Not yet, and its Terms don't allow protected health information without a written agreement. Nylas offers a pre-signed BAA on every paid plan.", + "Yes, on an Enterprise contract, which also turns on HIPAA mode. Its Terms don't allow protected health information without a signed agreement. Nylas offers a pre-signed BAA on every paid plan.", }, { question: "Do hosts have to reconnect their calendars?", diff --git a/apps/site/src/lib/pricing.ts b/apps/site/src/lib/pricing.ts index 4432f1539..1c26433b0 100644 --- a/apps/site/src/lib/pricing.ts +++ b/apps/site/src/lib/pricing.ts @@ -19,9 +19,6 @@ import { RETENTION_DAYS } from "@coss/retention"; const EMAIL: MeterKey = "email.recipients"; -/** Priced in the catalog, but the product hasn't shipped: public rate tables leave them out. */ -export const UNRELEASED_METERS: ReadonlySet = new Set(["video.participant_minutes"]); - /** The plans the estimator compares (Enterprise is priced per contract). */ export const ESTIMATED_PLANS = ["free", "pro", "scale"] as const satisfies readonly PlanKey[]; export type EstimatedPlan = (typeof ESTIMATED_PLANS)[number]; @@ -171,6 +168,8 @@ export const METER_NOTES: Partial> = { "30 inbox-days are an inbox for a month, charged by the day; every plan includes 3 inboxes", "hooks.messages": "One per message whatever its fan-out; retries, recoveries and test sends are free", + "scheduling.hosts": + "30 host-days are a host for a month, charged by the day from January 2027, beyond what your plan includes", "forms.file_uploads": `The files of accepted submissions, kept ${RETENTION_DAYS.formSubmissions} days; rounded up to the MB per submission`, }; @@ -179,7 +178,6 @@ export function rateTables(): RateTable[] { const card = RATE_CARDS.at(-1); if (!card) return []; return METER_KEYS.flatMap((meter): RateTable[] => { - if (UNRELEASED_METERS.has(meter)) return []; const definition = METERS[meter]; const rates = card.rates[meter]; if (!rates) return []; diff --git a/apps/site/test/billing-text.test.ts b/apps/site/test/billing-text.test.ts index ede09135e..43e321759 100644 --- a/apps/site/test/billing-text.test.ts +++ b/apps/site/test/billing-text.test.ts @@ -1,7 +1,12 @@ import { describe, expect, test } from "bun:test"; import { readFileSync } from "node:fs"; import { PLAN_KEYS } from "@coss/billing/catalog"; -import { renderBillingComponents } from "../src/lib/billing-text"; +import { + includedUnitsText, + planRows, + rateRows, + renderBillingComponents, +} from "../src/lib/billing-text"; describe("renderBillingComponents", () => { test("renders the plans and the usage rates from the catalog", () => { @@ -84,3 +89,25 @@ describe("renderBillingComponents", () => { expect(typed).toEqual([]); }); }); + +describe("what the plans say about Scheduling and HIPAA", () => { + const row = (label: string) => planRows().find((candidate) => candidate.label === label)?.values; + + test("host limits and what Scale includes", () => { + expect(row("Scheduling hosts")).toEqual(["3", "25", "Unlimited", "Custom"]); + expect(includedUnitsText("scheduling.hosts", "scale")).toBe("100 Scheduling hosts"); + expect(rateRows(["scheduling.hosts"])[0]?.rate).toBe("$0.99 per 30 host-days"); + }); + + test("HIPAA mode is by contract, on Enterprise only", () => { + expect(row("HIPAA mode with a BAA")).toEqual(["—", "—", "—", "By contract"]); + }); + + test("nothing about video", () => { + expect( + rateRows() + .map((candidate) => candidate.product) + .join(), + ).not.toMatch(/video/i); + }); +}); diff --git a/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/actions.ts b/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/actions.ts index 3ad4fad77..4459d41f0 100644 --- a/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/actions.ts +++ b/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/actions.ts @@ -32,6 +32,7 @@ import { redirect } from "next/navigation"; import type { ActionResult } from "@/lib/action-result"; import { ActionError, formString, runAction } from "@/lib/actions"; import { recordAudit } from "@/lib/audit"; +import { usageGate } from "@/lib/broadcasts"; import { schedulingDeps, schedulingUrl } from "@/lib/scheduling"; import { eventTypeChanges, issueLabel, parseJsonObject } from "@/lib/scheduling-edit"; import { type OrgContext, requireOrgPermission } from "@/lib/session"; @@ -129,6 +130,7 @@ export async function createHostAction( return runAction(async () => { const { context, env } = await contextFor(formData, "create"); const email = formString(formData, "email"); + const billing = await usageGate(); const host = await friendly(() => createHost( env, @@ -137,7 +139,11 @@ export async function createHostAction( time_zone: formString(formData, "timeZone"), ...(email ? { email } : {}), }, - { screen: configuredDeps().screenSetup }, + { + screen: configuredDeps().screenSetup, + plans: configuredDeps().plans, + billing, + }, ), ); await friendly(() => @@ -205,7 +211,10 @@ export async function updateHostLocationsAction(_prev: ActionResult | null, form export async function deleteHostAction(_prev: ActionResult | null, formData: FormData) { return runAction(async () => { const { context, env } = await contextFor(formData, "delete"); - const { id } = await friendly(() => deleteHost(env, formString(formData, "hostId"))); + const billing = await usageGate(); + const { id } = await friendly(() => + deleteHost(env, formString(formData, "hostId"), { billing }), + ); await audit(context, "scheduling_host.deleted", { type: "scheduling_host", id }); // From its own page, there's nothing left to come back to. if (formString(formData, "leavePage") === "on") { @@ -360,7 +369,7 @@ export async function createEventTypeAction( ? { locations: [{ type: "phone" as const, number: null }] } : {}), }, - { screen: configuredDeps().screenSetup }, + { screen: configuredDeps().screenSetup, plans: configuredDeps().plans }, ), ); await audit(context, "scheduling_event_type.created", { @@ -398,7 +407,10 @@ export async function updateEventTypeAction(_prev: ActionResult | null, formData const changes = eventTypeChanges(current, update); if (Object.keys(changes).length === 0) return null; const eventType = await friendly(() => - updateEventType(env, id, changes, { screen: configuredDeps().screenSetup }), + updateEventType(env, id, changes, { + screen: configuredDeps().screenSetup, + plans: configuredDeps().plans, + }), ); await audit(context, "scheduling_event_type.updated", { type: "scheduling_event_type", diff --git a/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/hosts/page.tsx b/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/hosts/page.tsx index feab10fd9..93924c65a 100644 --- a/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/hosts/page.tsx +++ b/apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/hosts/page.tsx @@ -1,4 +1,6 @@ -import { listHosts } from "@coss/scheduling"; +import { entitlementsFor, PLANS } from "@coss/billing"; +import { countLiveHosts, listHosts } from "@coss/scheduling"; +import { Alert, AlertDescription, AlertTitle } from "@coss/ui/components/alert"; import { Table, TableBody, @@ -28,17 +30,29 @@ export const metadata: Metadata = { title: "Hosts" }; const PAGE_SIZE = 50; +/** From this share of the plan's live hosts, the page says so before a create is refused. */ +const HOST_LIMIT_WARNING = 0.8; + export default async function HostsPage({ params, searchParams, }: PageProps<"/[orgSlug]/[projectSlug]/[envSlug]/scheduling/hosts">) { - const { schedulingPath, scope, can, target } = await schedulingPageContext(await params); + const { schedulingPath, scope, can, target, environment } = await schedulingPageContext( + await params, + ); const search = await searchParams; const after = typeof search.after === "string" ? search.after : undefined; const page = await listHosts(scope, { limit: PAGE_SIZE, after }); const last = page.data.at(-1); const hostsPath = `${schedulingPath}/hosts`; const canUpdate = can({ scheduling: ["update"] }); + const [hostLimit, liveHosts] = + environment.mode === "live" + ? await Promise.all([ + entitlementsFor(scope.organizationId).then((plan) => plan.limits.schedulingHosts), + countLiveHosts(scope.organizationId), + ]) + : [null, 0]; return (
+ {hostLimit !== null && liveHosts >= hostLimit * HOST_LIMIT_WARNING ? ( + + + {liveHosts.toLocaleString("en-US")} of {hostLimit.toLocaleString("en-US")} live hosts + + +

+ Your plan's limit counts every live environment. Hosts you have keep working; to add + more,{" "} + + move to {PLANS.scale.name} + + . +

+
+
+ ) : null} - {METER_KEYS.map((meter) => ( + {METER_KEYS.filter((meter) => rates[meter]).map((meter) => ( diff --git a/apps/web/src/app/admin/actions.ts b/apps/web/src/app/admin/actions.ts index 05e77422c..f25597c39 100644 --- a/apps/web/src/app/admin/actions.ts +++ b/apps/web/src/app/admin/actions.ts @@ -40,7 +40,7 @@ import type { ActionResult } from "@/lib/action-result"; import { ActionError, formString, runAction } from "@/lib/actions"; import { supportAudit } from "@/lib/audit"; import { auth } from "@/lib/auth"; -import { planHas } from "@/lib/billing"; +import { hasManagedContract, planHas } from "@/lib/billing"; import { riskHasher, signUpRiskScope } from "@/lib/risk"; import { requirePlatformAdminAction } from "@/lib/session"; import { isSystemOrganization, SYSTEM_ORGANIZATION_MESSAGE } from "@/lib/system-organization"; @@ -395,6 +395,12 @@ export async function enableHipaaModeAction( if (await isSystemOrganization(organizationId)) { throw new ActionError(SYSTEM_ORGANIZATION_MESSAGE); } + // Contract-only: a signed BAA goes with a contract, never a self-serve plan. + if (!(await hasManagedContract(organizationId))) { + throw new ActionError( + "HIPAA mode is contract-only: give this organization a sales contract first.", + ); + } if (!(await planHas(organizationId, "auditLog"))) { throw new ActionError( "HIPAA mode needs the audit log: put it on this organization's plan or contract first.", diff --git a/apps/web/src/app/api/mobile/v1/orgs/[orgSlug]/billing/plans/wire.ts b/apps/web/src/app/api/mobile/v1/orgs/[orgSlug]/billing/plans/wire.ts index 1bf5b18b1..59fbb1e34 100644 --- a/apps/web/src/app/api/mobile/v1/orgs/[orgSlug]/billing/plans/wire.ts +++ b/apps/web/src/app/api/mobile/v1/orgs/[orgSlug]/billing/plans/wire.ts @@ -82,7 +82,7 @@ export function toMobileAddOns(): PlansResponse["add_ons"] { export function toMobileUsagePrices(now: Date): PlansResponse["usage_prices"] { const rates = rateCardAt(now).rates; - return METER_KEYS.map((meter) => ({ + return METER_KEYS.filter((meter) => rates[meter]).map((meter) => ({ label: METERS[meter].label, rates: formatMeterRates(meter, rates[meter]), })); diff --git a/apps/web/src/lib/billing-preflight.ts b/apps/web/src/lib/billing-preflight.ts index e14dae753..306c1c47e 100644 --- a/apps/web/src/lib/billing-preflight.ts +++ b/apps/web/src/lib/billing-preflight.ts @@ -2,11 +2,12 @@ import "server-only"; import { FEATURE_LABELS, type FeatureKey, PLANS, type PlanKey } from "@coss/billing"; import { db } from "@coss/db"; import { hasLiveSubaccounts } from "@coss/email"; +import { hasLiveHostFilters } from "@coss/scheduling"; import { getOrganizationSsoProvider, hasScimConnection } from "@coss/tenancy"; /** How much of each feature the organization uses now (what a downgrade would leave behind). */ async function featureUsage(organizationId: string): Promise> { - const [sso, scim, drain, tracking, subaccount] = await Promise.all([ + const [sso, scim, drain, tracking, subaccount, hostFilters] = await Promise.all([ getOrganizationSsoProvider({ organizationId }), hasScimConnection({ organizationId }), db.query.logDrain.findFirst({ @@ -19,6 +20,7 @@ async function featureUsage(organizationId: string): Promise = { auditLog: "", dedicatedIps: "", subaccounts: "Delete your live email sub-accounts.", + schedulingHolds: "", + schedulingHostPortal: "", + schedulingRouting: "Remove host filters from your live event types (Scheduling).", + schedulingWhiteLabel: "", }; /** diff --git a/apps/web/src/lib/billing.ts b/apps/web/src/lib/billing.ts index fe56e8554..783021dff 100644 --- a/apps/web/src/lib/billing.ts +++ b/apps/web/src/lib/billing.ts @@ -57,12 +57,20 @@ export function planGate(): PlanGate { return { requireFeature: async (organizationId, feature) => (await gate()).requireFeature(organizationId, feature), + hasFeature: async (organizationId, feature) => + (await gate()).hasFeature(organizationId, feature), requireCapacity: async (organizationId, limit, current, count) => (await gate()).requireCapacity(organizationId, limit, current, count), limit: async (organizationId, limit) => (await gate()).limit(organizationId, limit), }; } +/** Whether a contract with sales (or coss.com's own) is in effect: never a self-serve plan. */ +export async function hasManagedContract(organizationId: string): Promise { + const { contract } = await billingStateAt(organizationId, new Date()); + return contract?.kind === "sales" || contract?.kind === "internal"; +} + /** The organization's plan rate limits (the rate limiter's plan layer). */ export async function planRules(organizationId: string) { return planRateLimits(organizationId, { systemOrganizationId: await systemOrganizationId() }); diff --git a/apps/web/src/lib/scheduling.ts b/apps/web/src/lib/scheduling.ts index a2feb4b69..b322f0e3a 100644 --- a/apps/web/src/lib/scheduling.ts +++ b/apps/web/src/lib/scheduling.ts @@ -4,6 +4,7 @@ import { riskHashKeyFromEnv } from "@coss/risk/runtime"; import type { SchedulingDeps } from "@coss/scheduling"; import { schedulingDataKeysFromEnv } from "@coss/scheduling/runtime"; import { env } from "@/env"; +import { planGate } from "@/lib/billing"; import { riskDeps } from "@/lib/risk"; /** Origin of the hosted pages sessions link to. */ @@ -24,6 +25,7 @@ export function schedulingDeps(): SchedulingDeps | null { // The API's key: bookings' `email_hmac` is written with it, and the list searches by it. lookupKey: Buffer.from(riskHashKeyFromEnv(env)), publicUrl: schedulingUrl(), + plans: planGate(), // Built when something is screened: most of these deps only read. screenSetup: (input) => hostedPageScreening(riskDeps())(input), }; diff --git a/apps/web/test/admin-actions.test.ts b/apps/web/test/admin-actions.test.ts index 807b92a64..1b508de98 100644 --- a/apps/web/test/admin-actions.test.ts +++ b/apps/web/test/admin-actions.test.ts @@ -13,7 +13,8 @@ mock.module("@coss/email", () => ({ })); mock.module("@coss/tenancy", () => ({ TenancyError: class extends Error {} })); const planHas = mock(async () => true); -mock.module("@/lib/billing", () => ({ planHas })); +const hasManagedContract = mock(async () => true); +mock.module("@/lib/billing", () => ({ hasManagedContract, planHas })); const audit = mock(async () => {}); const enableHipaaMode = mock(async () => {}); const disableHipaaMode = mock(async () => {}); @@ -870,6 +871,7 @@ describe("HIPAA mode", () => { disableHipaaMode.mockClear(); planHas.mockClear(); planHas.mockImplementation(async () => true); + hasManagedContract.mockImplementation(async () => true); isMemberOfHipaaOrganization.mockImplementation(async () => false); emailContext.hipaaMode = false; }); @@ -888,6 +890,13 @@ describe("HIPAA mode", () => { ); }); + test("it is contract-only: a self-serve plan can't have it, whatever it includes", async () => { + hasManagedContract.mockImplementation(async () => false); + const refused = await actions.enableHipaaModeAction(null, hipaaForm()); + expect(refused).toMatchObject({ ok: false, error: expect.stringContaining("contract-only") }); + expect(enableHipaaMode).not.toHaveBeenCalled(); + }); + test("it needs the audit log on the plan, and a reason", async () => { planHas.mockImplementation(async () => false); expect((await actions.enableHipaaModeAction(null, hipaaForm())).ok).toBe(false); diff --git a/apps/web/test/mobile-api.test.ts b/apps/web/test/mobile-api.test.ts index 76d874bba..bc3b76ac2 100644 --- a/apps/web/test/mobile-api.test.ts +++ b/apps/web/test/mobile-api.test.ts @@ -155,7 +155,7 @@ mock.module("@coss/sms", () => ({ }), })); const billingBannerState = mock(async () => ({ held: null, pastDue: true, allowance: null })); -mock.module("@/lib/billing", () => ({ billingBannerState })); +mock.module("@/lib/billing", () => ({ billingBannerState, planGate: () => ({}) })); let budgetHolds: { scope: string; targetId: string }[] = []; const billingModule = await import("@coss/billing"); mock.module("@coss/billing", () => ({ diff --git a/apps/worker/src/jobs.ts b/apps/worker/src/jobs.ts index b1c1de7df..9b0655199 100644 --- a/apps/worker/src/jobs.ts +++ b/apps/worker/src/jobs.ts @@ -65,6 +65,7 @@ import { import { RETENTION_DAYS } from "@coss/retention"; import { refreshDueDatasets } from "@coss/risk"; import { + chargeDueHostFees, checkHostEditedEvents, expireDueBookings, type NotificationDeps, @@ -252,6 +253,7 @@ export type WorkerDeps = { */ export const WORKER_LOOP_NAMES = [ "scheduling-expiry", + "scheduling-host-fees", "scheduling-notifications", "scheduling-calendar-sync", "scheduling-calendar-writes", @@ -748,6 +750,14 @@ export function startJobs(deps: WorkerDeps): Loop[] { intervalMs: 30_000, run: async () => (await expireDueBookings(scheduling)) === SCHEDULING_BATCH, }), + start({ + // A day of every live host (`scheduling.hosts`), charged when the day starts. + name: "scheduling-host-fees", + intervalMs: 10 * 60_000, + run: async () => + (await chargeDueHostFees({ limit: INBOX_FEE_BATCH, billing: deps.billing.usage })) >= + INBOX_FEE_BATCH, + }), ); const notifications = deps.schedulingNotifications; if (notifications) { diff --git a/packages/billing/AGENTS.md b/packages/billing/AGENTS.md index 401aff58a..12f882fba 100644 --- a/packages/billing/AGENTS.md +++ b/packages/billing/AGENTS.md @@ -45,7 +45,10 @@ repository root. dedicated IPs (`email.dedicated_pool`); sandbox never. Received email is `email.received` when it is stored (sandbox too: it is real mail; not the fake provider's, spam or viruses), inboxes `email.inboxes` per day a live one exists (priced per 30 inbox-days; see `packages/email/AGENTS.md`). Hooks is `hooks.messages`, one per live - message accepted whatever its fan-out, with Free capped and paid plans including more (`packages/hooks/AGENTS.md`). + message accepted whatever its fan-out, with Free capped and paid plans including more (`packages/hooks/AGENTS.md`). Scheduling is + `scheduling.bookings` per booking and `scheduling.hosts` per day a live host exists, priced per 30 host-days + (`packages/scheduling/AGENTS.md`). A meter that starts charging for something already running gets its first rate on + a future card (`meterPricedFrom`), for the Terms' 30 days' notice. - **Attribution and spend** (`billing_usage_attribution`, `./spend`): `recordUsage` also writes who caused the usage (project, API key, and the member a personal key belongs to — `email.api_key_owner_id`, captured at acceptance, so revoking a key never erases a member's spend), same hour and shard as `billing_usage`, which diff --git a/packages/billing/src/catalog/entitlements.ts b/packages/billing/src/catalog/entitlements.ts index 20ba2c3c6..a3b706e13 100644 --- a/packages/billing/src/catalog/entitlements.ts +++ b/packages/billing/src/catalog/entitlements.ts @@ -8,6 +8,10 @@ export const FEATURE_KEYS = [ "scim", "dedicatedIps", "subaccounts", + "schedulingHolds", + "schedulingHostPortal", + "schedulingRouting", + "schedulingWhiteLabel", ] as const; export type FeatureKey = (typeof FEATURE_KEYS)[number]; @@ -19,6 +23,9 @@ export const LIMIT_KEYS = [ "smsNumbers", "inboxes", "subaccounts", + "schedulingHosts", + "schedulingSearchHosts", + "schedulingGroupSeats", ] as const; export type LimitKey = (typeof LIMIT_KEYS)[number]; @@ -29,6 +36,7 @@ export const RATE_LIMIT_KEYS = [ "email.marketing.recipients.second", "sms.segments.second", "sms.segments.day", + "scheduling.slots.second", ] as const; export type RateLimitKey = (typeof RATE_LIMIT_KEYS)[number]; @@ -56,6 +64,10 @@ export const FEATURE_LABELS: Record = { scim: "Directory sync (SCIM)", dedicatedIps: "Dedicated IPs", subaccounts: "Email for platforms (sub-accounts)", + schedulingHolds: "Scheduling holds", + schedulingHostPortal: "Scheduling host portal", + schedulingRouting: "Scheduling attribute routing", + schedulingWhiteLabel: "Scheduling pages without coss.com's name", }; export const LIMIT_LABELS: Record = { @@ -66,6 +78,9 @@ export const LIMIT_LABELS: Record = { smsNumbers: "SMS phone numbers", inboxes: "Inboxes", subaccounts: "Email sub-accounts", + schedulingHosts: "Scheduling hosts", + schedulingSearchHosts: "Hosts per availability search", + schedulingGroupSeats: "Seats per group session", }; const SECOND = 1000; diff --git a/packages/billing/src/catalog/meters.ts b/packages/billing/src/catalog/meters.ts index 30439b077..8d5c65a1f 100644 --- a/packages/billing/src/catalog/meters.ts +++ b/packages/billing/src/catalog/meters.ts @@ -7,7 +7,6 @@ export const PRODUCTS = [ "risk", "verify", "forms", - "video", "scheduling", "hooks", ] as const; @@ -179,15 +178,6 @@ export const METERS = { requires: "plan", measure: true, }, - "video.participant_minutes": { - product: "video", - label: "Video", - unit: "participant-minute", - units: "participant-minutes", - displayPer: 1000, - dimensions: [], - requires: "plan", - }, "scheduling.bookings": { product: "scheduling", label: "Scheduling", @@ -197,6 +187,17 @@ export const METERS = { dimensions: [], requires: "plan", }, + // Live hosts, per day each one exists (a day is charged when it starts: on creation, then daily). + // Priced per 30 host-days, a host for a month. + "scheduling.hosts": { + product: "scheduling", + label: "Scheduling hosts", + unit: "host-day", + units: "host-days", + displayPer: 30, + dimensions: [], + requires: "plan", + }, // Hooks: a live message accepted, once whatever its fan-out. Retries, recoveries, test sends and // sandbox are free; delivery quotas bound the fan-out. "hooks.messages": { diff --git a/packages/billing/src/catalog/plans.ts b/packages/billing/src/catalog/plans.ts index 5510e1811..43cbac811 100644 --- a/packages/billing/src/catalog/plans.ts +++ b/packages/billing/src/catalog/plans.ts @@ -2,8 +2,8 @@ * The plans. A plan's monthly fee is credit spendable on any product's usage, so a month costs * the larger of the fee and the usage (plus add-ons). Free has no credit but includes 3,000 * emails, 1,000 form submissions, 1,000 MB of their uploads and 50,000 Hooks messages a month and - * blocks usage beyond its allowance. Every plan includes 100 phone checks, 3 inboxes and Hooks - * messages a month. + * blocks usage beyond its allowance. Every plan includes 100 phone checks, 3 inboxes, Hooks + * messages and Scheduling hosts a month. */ import { type Entitlements, perDay, perSecond } from "./entitlements.ts"; import type { MeterKey } from "./meters.ts"; @@ -20,6 +20,14 @@ const HOOKS_MESSAGES_INCLUDED = { enterprise: 50_000_000, } as const; +/** Host-days each plan includes each month: its hosts for the longest month. */ +const SCHEDULING_HOST_DAYS_INCLUDED = { + free: 3 * 31, + pro: 25 * 31, + scale: 100 * 31, + enterprise: 100 * 31, +} as const; + export const PLAN_KEYS = ["free", "pro", "scale", "enterprise"] as const; export type PlanKey = (typeof PLAN_KEYS)[number]; @@ -61,6 +69,7 @@ export const PLANS = { "email.inboxes": INBOX_DAYS_INCLUDED, "risk.phone_checks": PHONE_CHECKS_INCLUDED, "hooks.messages": HOOKS_MESSAGES_INCLUDED.free, + "scheduling.hosts": SCHEDULING_HOST_DAYS_INCLUDED.free, }, overage: "blocked", termMonths: null, @@ -73,6 +82,10 @@ export const PLANS = { scim: false, dedicatedIps: false, subaccounts: false, + schedulingHolds: false, + schedulingHostPortal: false, + schedulingRouting: false, + schedulingWhiteLabel: false, }, limits: { domains: 1, @@ -82,6 +95,9 @@ export const PLANS = { smsNumbers: 0, inboxes: 3, subaccounts: 0, + schedulingHosts: 3, + schedulingSearchHosts: 5, + schedulingGroupSeats: 10, }, rateLimits: { "email.recipients.second": perSecond(2, 50), @@ -89,6 +105,7 @@ export const PLANS = { "email.marketing.recipients.second": perSecond(2, 10), "sms.segments.second": perSecond(1, 5), "sms.segments.day": perDay(100), + "scheduling.slots.second": perSecond(5, 20), }, }, highlights: ["3,000 emails per month", "1 domain", "3 team members", "3 inboxes"], @@ -104,6 +121,7 @@ export const PLANS = { "email.inboxes": INBOX_DAYS_INCLUDED, "risk.phone_checks": PHONE_CHECKS_INCLUDED, "hooks.messages": HOOKS_MESSAGES_INCLUDED.pro, + "scheduling.hosts": SCHEDULING_HOST_DAYS_INCLUDED.pro, }, overage: "billed", termMonths: null, @@ -116,6 +134,10 @@ export const PLANS = { scim: false, dedicatedIps: false, subaccounts: false, + schedulingHolds: true, + schedulingHostPortal: false, + schedulingRouting: false, + schedulingWhiteLabel: false, }, limits: { domains: 10, @@ -125,6 +147,9 @@ export const PLANS = { smsNumbers: 5, inboxes: 1_000, subaccounts: 0, + schedulingHosts: 25, + schedulingSearchHosts: 25, + schedulingGroupSeats: 50, }, rateLimits: { "email.recipients.second": perSecond(14, 100), @@ -132,6 +157,7 @@ export const PLANS = { "email.marketing.recipients.second": perSecond(10, 50), "sms.segments.second": perSecond(10, 50), "sms.segments.day": perDay(10_000), + "scheduling.slots.second": perSecond(25, 100), }, }, highlights: [ @@ -154,6 +180,7 @@ export const PLANS = { "email.inboxes": INBOX_DAYS_INCLUDED, "risk.phone_checks": PHONE_CHECKS_INCLUDED, "hooks.messages": HOOKS_MESSAGES_INCLUDED.scale, + "scheduling.hosts": SCHEDULING_HOST_DAYS_INCLUDED.scale, }, overage: "billed", termMonths: null, @@ -166,6 +193,10 @@ export const PLANS = { scim: false, dedicatedIps: false, subaccounts: true, + schedulingHolds: true, + schedulingHostPortal: true, + schedulingRouting: true, + schedulingWhiteLabel: true, }, limits: { domains: 100, @@ -175,6 +206,9 @@ export const PLANS = { smsNumbers: 50, inboxes: 10_000, subaccounts: 1_000, + schedulingHosts: null, + schedulingSearchHosts: 500, + schedulingGroupSeats: 10_000, }, rateLimits: { "email.recipients.second": perSecond(50, 200), @@ -182,6 +216,7 @@ export const PLANS = { "email.marketing.recipients.second": perSecond(50, 250), "sms.segments.second": perSecond(30, 150), "sms.segments.day": perDay(100_000), + "scheduling.slots.second": perSecond(100, 300), }, }, highlights: [ @@ -190,6 +225,7 @@ export const PLANS = { "Unlimited team members", "Log drains", "Single sign-on", + "100 Scheduling hosts, more by the month", ], }, enterprise: { @@ -203,6 +239,7 @@ export const PLANS = { "email.inboxes": INBOX_DAYS_INCLUDED, "risk.phone_checks": PHONE_CHECKS_INCLUDED, "hooks.messages": HOOKS_MESSAGES_INCLUDED.enterprise, + "scheduling.hosts": SCHEDULING_HOST_DAYS_INCLUDED.enterprise, }, overage: "billed", termMonths: 12, @@ -215,6 +252,10 @@ export const PLANS = { scim: true, dedicatedIps: false, subaccounts: true, + schedulingHolds: true, + schedulingHostPortal: true, + schedulingRouting: true, + schedulingWhiteLabel: true, }, limits: { domains: null, @@ -224,6 +265,9 @@ export const PLANS = { smsNumbers: null, inboxes: null, subaccounts: null, + schedulingHosts: null, + schedulingSearchHosts: 500, + schedulingGroupSeats: 10_000, }, rateLimits: { "email.recipients.second": perSecond(100, 500), @@ -231,11 +275,13 @@ export const PLANS = { "email.marketing.recipients.second": perSecond(100, 500), "sms.segments.second": perSecond(100, 500), "sms.segments.day": perDay(1_000_000), + "scheduling.slots.second": perSecond(200, 1000), }, }, highlights: [ "Volume pricing and commitments", "Directory sync (SCIM)", + "HIPAA mode with a signed BAA", "Invoicing and direct debit", ], }, diff --git a/packages/billing/src/catalog/rates.ts b/packages/billing/src/catalog/rates.ts index 09dc19114..4e4b75a8b 100644 --- a/packages/billing/src/catalog/rates.ts +++ b/packages/billing/src/catalog/rates.ts @@ -96,8 +96,6 @@ const LAUNCH: RateCard = { // meters were added; no organization had used them). "forms.submissions": { default: flatRate(1_000_000n) }, "forms.file_uploads": { default: flatRate(250_000n) }, - // $1.50 per 1,000 participant-minutes. - "video.participant_minutes": { default: flatRate(1_500_000n) }, // $0.10 per booking. "scheduling.bookings": { default: flatRate(100_000_000n) }, }, @@ -138,7 +136,8 @@ const NOVEMBER_2026: RateCard = { }; const JANUARY_2027: RateCard = { - // Twilio's price changes: SMS to Canada and SMS received in the US. + // Twilio's price changes: SMS to Canada and SMS received in the US. Also the first prices of SMS + // received on Canadian numbers and of Scheduling hosts (the Terms' 30 days' notice). effectiveFrom: new Date("2027-01-01T00:00:00Z"), rates: { ...NOVEMBER_2026.rates, @@ -147,6 +146,8 @@ const JANUARY_2027: RateCard = { byDimension: flatRates("country", SMS_PRICES_2027_01_01.inbound), }, "verify.sms": { byDimension: flatRates("country", SMS_PRICES_2027_01_01.outbound) }, + // A host for 30 days: $0.99 beyond what the plan includes. Per day: $0.033. + "scheduling.hosts": { default: flatRate(33_000_000n) }, }, }; @@ -161,6 +162,14 @@ export function rateCardAt(at: Date, cards: readonly RateCard[] = RATE_CARDS): R return current; } +/** When list prices first price `meter`: until then its usage must not be recorded as charged. */ +export function meterPricedFrom( + meter: MeterKey, + cards: readonly RateCard[] = RATE_CARDS, +): Date | null { + return cards.find((card) => card.rates[meter])?.effectiveFrom ?? null; +} + /** The rate for one dimension value: its own, else the meter's default. */ export function rateFor(rates: MeterRates | undefined, dimension: string): Rate | null { return rates?.byDimension?.[dimension] ?? rates?.default ?? null; diff --git a/packages/billing/src/catalog/sms-prices-2027-01-01.generated.ts b/packages/billing/src/catalog/sms-prices-2027-01-01.generated.ts index 0fecd9291..1dd17c487 100644 --- a/packages/billing/src/catalog/sms-prices-2027-01-01.generated.ts +++ b/packages/billing/src/catalog/sms-prices-2027-01-01.generated.ts @@ -15,6 +15,7 @@ export const SMS_PRICES_2027_01_01: { US: 14_200_000n, }, inbound: { + CA: 10_400_000n, US: 16_000_000n, }, }; diff --git a/packages/billing/src/enforcement.ts b/packages/billing/src/enforcement.ts index 82f0b0a68..cc0dca7ea 100644 --- a/packages/billing/src/enforcement.ts +++ b/packages/billing/src/enforcement.ts @@ -30,6 +30,16 @@ export function planWith(predicate: (entitlements: Entitlements) => boolean): Pl return PLAN_KEYS.find((key) => predicate(PLANS[key].entitlements)) ?? "enterprise"; } +/** Features whose label reads as a plural, for "are available" in refusals. */ +const PLURAL_FEATURES: ReadonlySet = new Set([ + "trackingDomains", + "logDrains", + "dedicatedIps", + "subaccounts", + "schedulingHolds", + "schedulingWhiteLabel", +]); + export function featureUnavailable(feature: FeatureKey): BillingError { const addOn = addOnGranting(feature); if (addOn) { @@ -40,9 +50,10 @@ export function featureUnavailable(feature: FeatureKey): BillingError { ); } const plan = PLANS[planWith((e) => e.features[feature])].name; + const plural = PLURAL_FEATURES.has(feature); return new BillingError( "plan_feature_unavailable", - `${FEATURE_LABELS[feature]} ${feature === "auditLog" ? "is" : "are"} available on the ${plan} plan and above.`, + `${FEATURE_LABELS[feature]} ${plural ? "are" : "is"} available on the ${plan} plan and above.`, ); } @@ -70,6 +81,10 @@ export function createPlanGate(opts: { systemOrganizationId?: string | null } = const entitlements = await entitlementsFor(organizationId); if (!entitlements.features[feature]) throw featureUnavailable(feature); }, + async hasFeature(organizationId, feature) { + if (exempt(organizationId)) return true; + return (await entitlementsFor(organizationId)).features[feature]; + }, async requireCapacity(organizationId, limit, current, count = 1) { if (exempt(organizationId)) return; const value = (await entitlementsFor(organizationId)).limits[limit]; diff --git a/packages/billing/src/gate.ts b/packages/billing/src/gate.ts index 80de41884..b33414562 100644 --- a/packages/billing/src/gate.ts +++ b/packages/billing/src/gate.ts @@ -12,6 +12,8 @@ export type { FeatureKey, LimitKey, MeterKey }; export interface PlanGate { /** Throws a `BillingError` (`plan_feature_unavailable`) when the plan lacks the feature. */ requireFeature(organizationId: string, feature: FeatureKey): Promise; + /** Whether the plan has the feature, for what changes rather than refuses. */ + hasFeature(organizationId: string, feature: FeatureKey): Promise; /** * Throws a `BillingError` (`plan_limit_reached`) when `count` more would exceed the plan's * limit, `current` already existing. diff --git a/packages/billing/src/metering.ts b/packages/billing/src/metering.ts index f0a58af72..03acfa990 100644 --- a/packages/billing/src/metering.ts +++ b/packages/billing/src/metering.ts @@ -2,9 +2,10 @@ * Metering: what products call, in the transaction that makes their usage billable (an email * becoming `sent`). Imports only `@coss/db`, so any product package can depend on it. */ -import { db, type Executor, schema } from "@coss/db"; +import { db, type Executor, schema, type Transaction } from "@coss/db"; import { type AnyColumn, and, eq, exists, inArray, or, type SQL, sql } from "drizzle-orm"; import type { MeterKey } from "./catalog/meters.ts"; +import type { UsageGate } from "./gate.ts"; const { billingBudgetHold, billingHold, billingUsage, billingUsageAttribution } = schema; @@ -87,6 +88,33 @@ export async function recordUsage(tx: Executor, input: UsageInput): Promise { + for (let day = 0; day < input.days; day++) { + try { + await (tx as Transaction).transaction((savepoint) => + gate.reserve(savepoint, { + organizationId: input.organizationId, + meter: input.meter, + quantity: 1, + }), + ); + } catch (error) { + const code = (error as { code?: string } | null)?.code; + if (code === "usage_limit_exceeded" || code === "plan_feature_unavailable") return; + throw error; + } + } +} + /** Whether live sending is held for billing (any reason). */ export async function isOnBillingHold( organizationId: string, diff --git a/packages/billing/test/engine.test.ts b/packages/billing/test/engine.test.ts index 4422412b7..738a6d040 100644 --- a/packages/billing/test/engine.test.ts +++ b/packages/billing/test/engine.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; -import type { MeterKey } from "../src/catalog/meters.ts"; -import { RATE_CARDS, rateCardAt } from "../src/catalog/rates.ts"; +import { dimensionKey, type MeterKey } from "../src/catalog/meters.ts"; +import { PLANS } from "../src/catalog/plans.ts"; +import { meterPricedFrom, RATE_CARDS, rateCardAt, rateFor } from "../src/catalog/rates.ts"; import { chargeRange, composeContractChange, @@ -127,8 +128,8 @@ describe("terms", () => { "verify.sms": 0, "forms.submissions": 1_000, "forms.file_uploads": 1_000, - "video.participant_minutes": 0, "scheduling.bookings": 100, + "scheduling.hosts": 93, "hooks.messages": 50_000, }); expect(free.fee).toBeNull(); @@ -149,9 +150,9 @@ describe("terms", () => { expect(meterAvailability(pro, "sms.segments", "country=GB")).toBe("not_priced"); expect(meterAvailability(terms({ version: 1, plan: "free" }), "sms.segments")).toBe("blocked"); expect(meterAvailability(pro, "email.recipients")).toBe("available"); - expect( - meterAvailability(terms({ version: 1, plan: "free" }), "video.participant_minutes"), - ).toBe("blocked"); + expect(meterAvailability(terms({ version: 1, plan: "free" }), "email.dedicated_ip")).toBe( + "blocked", + ); }); test("a contract's rates for some dimension values keep the list price of the others", () => { @@ -1026,3 +1027,36 @@ describe("internal contracts", () => { expect(invoice.lines.map((line) => line.kind)).toEqual(["usage", "write_off"]); }); }); + +describe("the catalog", () => { + test("every country numbers are sold in has an inbound SMS price from January 2027", () => { + const january = rateCardAt(new Date("2027-01-01T00:00:00Z")); + for (const country of ["US", "CA"]) { + expect( + rateFor(january.rates["sms.received_segments"], dimensionKey({ country })), + ).not.toBeNull(); + } + // Starting a charge takes the Terms' 30 days' notice: until then Canada stays uncharged. + const november = rateCardAt(new Date("2026-12-31T23:59:59Z")); + expect( + rateFor(november.rates["sms.received_segments"], dimensionKey({ country: "CA" })), + ).toBeNull(); + }); + + test("Scheduling hosts are first priced in January 2027", () => { + expect(meterPricedFrom("scheduling.hosts")).toEqual(new Date("2027-01-01T00:00:00Z")); + expect(rateCardAt(new Date("2026-12-31T00:00:00Z")).rates["scheduling.hosts"]).toBeUndefined(); + expect(meterPricedFrom("email.recipients")).toEqual(new Date("2026-01-01T00:00:00Z")); + }); + + test("plans include hosts up to their limit, and Scale meters beyond its allowance", () => { + expect(PLANS.free.includedUnits["scheduling.hosts"]).toBe( + (PLANS.free.entitlements.limits.schedulingHosts as number) * 31, + ); + expect(PLANS.pro.includedUnits["scheduling.hosts"]).toBe( + (PLANS.pro.entitlements.limits.schedulingHosts as number) * 31, + ); + expect(PLANS.scale.entitlements.limits.schedulingHosts).toBeNull(); + expect(PLANS.scale.includedUnits["scheduling.hosts"]).toBe(100 * 31); + }); +}); diff --git a/packages/compliance/AGENTS.md b/packages/compliance/AGENTS.md index b900fa64b..93fd28d47 100644 --- a/packages/compliance/AGENTS.md +++ b/packages/compliance/AGENTS.md @@ -10,8 +10,9 @@ where PHI can go and who at coss.com can reach it, and shows the customer what t `enableHipaaMode`/`disableHipaaMode` (`@coss/tenancy/admin`, from the admin console's organization page, audited in `admin_audit_event` and through `supportAudit` in the customer's own log). It is **never a billing entitlement**: a contract ending or a scheduled downgrade must not lift - safeguards with nobody deciding to. Sales bills it as a custom add-on on the contract. Turning it - on needs the plan's `auditLog`, the BAA's date and reference, and a reason; it also sets + safeguards with nobody deciding to. It is **contract-only**: `enableHipaaModeAction` refuses without a + `sales` or `internal` contract in effect (`hasManagedContract`), and sales bills it as a custom add-on on that + contract. Turning it on needs the plan's `auditLog`, the BAA's date and reference, and a reason; it also sets `requireTwoFactor` and updates what's waiting to go out (emails' snapshots, SMS bodies, pending pushes) in the same transaction. - **The catalog** (`./catalog`, client-safe, no imports): `HIPAA_CONTROLS`, `HIPAA_VENDORS`, diff --git a/packages/db/migrations/0128_chemical_echo.sql b/packages/db/migrations/0128_chemical_echo.sql new file mode 100644 index 000000000..fb21c8eb1 --- /dev/null +++ b/packages/db/migrations/0128_chemical_echo.sql @@ -0,0 +1,5 @@ +ALTER TABLE "scheduling_host" ADD COLUMN "next_fee_at" timestamp with time zone;--> statement-breakpoint +CREATE INDEX "scheduling_host_next_fee_at_idx" ON "scheduling_host" USING btree ("next_fee_at") WHERE "scheduling_host"."next_fee_at" IS NOT NULL;--> statement-breakpoint +CREATE INDEX "scheduling_host_live_organization_idx" ON "scheduling_host" USING btree ("organization_id") WHERE "scheduling_host"."next_fee_at" IS NOT NULL;--> statement-breakpoint +-- Hosts that already exist are charged from when the meter is first priced. +UPDATE "scheduling_host" SET "next_fee_at" = '2027-01-01T00:00:00Z' FROM "environment" WHERE "environment"."id" = "scheduling_host"."environment_id" AND "environment"."mode" = 'live'; diff --git a/packages/db/migrations/meta/0128_snapshot.json b/packages/db/migrations/meta/0128_snapshot.json new file mode 100644 index 000000000..7c4244504 --- /dev/null +++ b/packages/db/migrations/meta/0128_snapshot.json @@ -0,0 +1,31202 @@ +{ + "id": "75cfe704-fcdf-4912-bb1c-f1e78c4c7728", + "prevId": "d09f1f6e-9294-45c3-8700-b4db33285970", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_configId_idx": { + "name": "apikey_configId_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_referenceId_idx": { + "name": "apikey_referenceId_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_two_factor": { + "name": "require_two_factor", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "require_sso": { + "name": "require_sso", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.passkey": { + "name": "passkey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backed_up": { + "name": "backed_up", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + { + "expression": "credential_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_connection_binding": { + "name": "scim_connection_binding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_key": { + "name": "connection_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommission_status": { + "name": "decommission_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "decommission_cursor_user_id": { + "name": "decommission_cursor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommission_reconciled_user_count": { + "name": "decommission_reconciled_user_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_batch_count": { + "name": "decommission_batch_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_revision": { + "name": "decommission_revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "decommission_completed_at": { + "name": "decommission_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommission_lease_id": { + "name": "decommission_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommission_lease_expires_at": { + "name": "decommission_lease_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scimConnectionBinding_connectionId_idx": { + "name": "scimConnectionBinding_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_connection_binding_connection_key_unique": { + "name": "scim_connection_binding_connection_key_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group": { + "name": "scim_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name_key": { + "name": "display_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimGroup_connectionId_idx": { + "name": "scimGroup_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimGroup_provisioningDomainId_idx": { + "name": "scimGroup_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_group_display_name_key_unique": { + "name": "scim_group_display_name_key_unique", + "nullsNotDistinct": false, + "columns": [ + "display_name_key" + ] + }, + "scim_group_external_id_key_unique": { + "name": "scim_group_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + }, + "scim_group_order_key_unique": { + "name": "scim_group_order_key_unique", + "nullsNotDistinct": false, + "columns": [ + "order_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_member": { + "name": "scim_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "membership_key": { + "name": "membership_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimGroupMember_connectionId_idx": { + "name": "scimGroupMember_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimGroupMember_groupId_idx": { + "name": "scimGroupMember_groupId_idx", + "columns": [ + { + "expression": "group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimGroupMember_scimUserId_idx": { + "name": "scimGroupMember_scimUserId_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_group_member_group_id_scim_group_id_fk": { + "name": "scim_group_member_group_id_scim_group_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_group", + "columnsFrom": [ + "group_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_group_member_scim_user_id_scim_user_id_fk": { + "name": "scim_group_member_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_group_member", + "tableTo": "scim_user", + "columnsFrom": [ + "scim_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_group_member_membership_key_unique": { + "name": "scim_group_member_membership_key_unique", + "nullsNotDistinct": false, + "columns": [ + "membership_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_identity_tombstone": { + "name": "scim_identity_tombstone", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile": { + "name": "profile", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimIdentityTombstone_connectionId_idx": { + "name": "scimIdentityTombstone_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimIdentityTombstone_provisioningDomainId_idx": { + "name": "scimIdentityTombstone_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimIdentityTombstone_userId_idx": { + "name": "scimIdentityTombstone_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_identity_tombstone_user_id_user_id_fk": { + "name": "scim_identity_tombstone_user_id_user_id_fk", + "tableFrom": "scim_identity_tombstone", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_identity_tombstone_external_id_key_unique": { + "name": "scim_identity_tombstone_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_managed_connection": { + "name": "scim_managed_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "creation_request_id": { + "name": "creation_request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "decommission_started_at": { + "name": "decommission_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommission_started_by": { + "name": "decommission_started_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "decommissioned_by": { + "name": "decommissioned_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scimManagedConnection_provisioningDomainId_idx": { + "name": "scimManagedConnection_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_managed_connection_creation_request_id_unique": { + "name": "scim_managed_connection_creation_request_id_unique", + "nullsNotDistinct": false, + "columns": [ + "creation_request_id" + ] + }, + "scim_managed_connection_connection_id_unique": { + "name": "scim_managed_connection_connection_id_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_managed_connection_event": { + "name": "scim_managed_connection_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_record_id": { + "name": "connection_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimManagedConnectionEvent_connectionRecordId_idx": { + "name": "scimManagedConnectionEvent_connectionRecordId_idx", + "columns": [ + { + "expression": "connection_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_managed_connection_event_connection_record_id_scim_managed_connection_id_fk": { + "name": "scim_managed_connection_event_connection_record_id_scim_managed_connection_id_fk", + "tableFrom": "scim_managed_connection_event", + "tableTo": "scim_managed_connection", + "columnsFrom": [ + "connection_record_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_managed_connection_event_event_key_unique": { + "name": "scim_managed_connection_event_event_key_unique", + "nullsNotDistinct": false, + "columns": [ + "event_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_managed_credential": { + "name": "scim_managed_credential", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_record_id": { + "name": "connection_record_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_digest": { + "name": "token_digest", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash_version": { + "name": "hash_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_slot_key": { + "name": "active_slot_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serialized_scopes": { + "name": "serialized_scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decommissioned_at": { + "name": "decommissioned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "scimManagedCredential_connectionRecordId_idx": { + "name": "scimManagedCredential_connectionRecordId_idx", + "columns": [ + { + "expression": "connection_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_managed_credential_connection_record_id_scim_managed_connection_id_fk": { + "name": "scim_managed_credential_connection_record_id_scim_managed_connection_id_fk", + "tableFrom": "scim_managed_credential", + "tableTo": "scim_managed_connection", + "columnsFrom": [ + "connection_record_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_managed_credential_credential_id_unique": { + "name": "scim_managed_credential_credential_id_unique", + "nullsNotDistinct": false, + "columns": [ + "credential_id" + ] + }, + "scim_managed_credential_active_slot_key_unique": { + "name": "scim_managed_credential_active_slot_key_unique", + "nullsNotDistinct": false, + "columns": [ + "active_slot_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_projection_grant": { + "name": "scim_projection_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scim_user_id": { + "name": "scim_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_value": { + "name": "source_value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_key": { + "name": "grant_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimProjectionGrant_connectionId_idx": { + "name": "scimProjectionGrant_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimProjectionGrant_provisioningDomainId_idx": { + "name": "scimProjectionGrant_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimProjectionGrant_scimUserId_idx": { + "name": "scimProjectionGrant_scimUserId_idx", + "columns": [ + { + "expression": "scim_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimProjectionGrant_userId_idx": { + "name": "scimProjectionGrant_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_projection_grant_scim_user_id_scim_user_id_fk": { + "name": "scim_projection_grant_scim_user_id_scim_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "scim_user", + "columnsFrom": [ + "scim_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scim_projection_grant_user_id_user_id_fk": { + "name": "scim_projection_grant_user_id_user_id_fk", + "tableFrom": "scim_projection_grant", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_projection_grant_grant_key_unique": { + "name": "scim_projection_grant_grant_key_unique", + "nullsNotDistinct": false, + "columns": [ + "grant_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_subject": { + "name": "scim_subject", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_source_id": { + "name": "profile_source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimSubject_profileSourceId_idx": { + "name": "scimSubject_profileSourceId_idx", + "columns": [ + { + "expression": "profile_source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_subject_user_id_user_id_fk": { + "name": "scim_subject_user_id_user_id_fk", + "tableFrom": "scim_subject", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_subject_user_id_unique": { + "name": "scim_subject_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_user": { + "name": "scim_user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provisioning_domain_id": { + "name": "provisioning_domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_user_key": { + "name": "connection_user_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_name": { + "name": "user_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_name_key": { + "name": "user_name_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary_email": { + "name": "primary_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "work_email_value_index": { + "name": "work_email_value_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_value_index": { + "name": "email_value_index", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "formatted_name": { + "name": "formatted_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "given_name": { + "name": "given_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "family_name": { + "name": "family_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "serialized_emails": { + "name": "serialized_emails", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "serialized_attributes": { + "name": "serialized_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id_key": { + "name": "external_id_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "order_key": { + "name": "order_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scimUser_connectionId_idx": { + "name": "scimUser_connectionId_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimUser_provisioningDomainId_idx": { + "name": "scimUser_provisioningDomainId_idx", + "columns": [ + { + "expression": "provisioning_domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scimUser_userId_idx": { + "name": "scimUser_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scim_user_user_id_user_id_fk": { + "name": "scim_user_user_id_user_id_fk", + "tableFrom": "scim_user", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_user_connection_user_key_unique": { + "name": "scim_user_connection_user_key_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_user_key" + ] + }, + "scim_user_user_name_key_unique": { + "name": "scim_user_user_name_key_unique", + "nullsNotDistinct": false, + "columns": [ + "user_name_key" + ] + }, + "scim_user_external_id_key_unique": { + "name": "scim_user_external_id_key_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id_key" + ] + }, + "scim_user_order_key_unique": { + "name": "scim_user_order_key_unique", + "nullsNotDistinct": false, + "columns": [ + "order_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sso_provider_id": { + "name": "sso_provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_verified": { + "name": "domain_verified", + "type": "boolean", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sso_provider_provider_id_unique": { + "name": "sso_provider_provider_id_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.two_factor": { + "name": "two_factor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "backup_codes": { + "name": "backup_codes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "failed_verification_count": { + "name": "failed_verification_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "locked_until": { + "name": "locked_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "twoFactor_secret_idx": { + "name": "twoFactor_secret_idx", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "twoFactor_userId_idx": { + "name": "twoFactor_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "two_factor_user_id_user_id_fk": { + "name": "two_factor_user_id_user_id_fk", + "tableFrom": "two_factor", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "two_factor_enabled": { + "name": "two_factor_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "canonical_email": { + "name": "canonical_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owned_organization_limit": { + "name": "owned_organization_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_canonicalEmail_idx": { + "name": "user_canonicalEmail_idx", + "columns": [ + { + "expression": "canonical_email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_account": { + "name": "billing_account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "billing_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_customer_id": { + "name": "provider_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collection_method": { + "name": "collection_method", + "type": "billing_collection_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'charge_automatically'" + }, + "net_terms_days": { + "name": "net_terms_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 30 + }, + "allowed_payment_methods": { + "name": "allowed_payment_methods", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{card}'::text[]" + }, + "required_payment_methods": { + "name": "required_payment_methods", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "tax_mode": { + "name": "tax_mode", + "type": "billing_tax_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "billing_email": { + "name": "billing_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_cc": { + "name": "billing_cc", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "billing_name": { + "name": "billing_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_address": { + "name": "billing_address", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "tax_id": { + "name": "tax_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spend_limit_cents": { + "name": "spend_limit_cents", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "alert_thresholds": { + "name": "alert_thresholds", + "type": "integer[]", + "primaryKey": false, + "notNull": true, + "default": "'{80,100}'::integer[]" + }, + "grace_days": { + "name": "grace_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 14 + }, + "status": { + "name": "status", + "type": "billing_account_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "past_due_since": { + "name": "past_due_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hold_exempt_until": { + "name": "hold_exempt_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payment_method": { + "name": "payment_method", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_account_organization_id_organization_id_fk": { + "name": "billing_account_organization_id_organization_id_fk", + "tableFrom": "billing_account", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_account_organization_unique": { + "name": "billing_account_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_budget": { + "name": "billing_budget", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "billing_budget_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "alert_thresholds": { + "name": "alert_thresholds", + "type": "integer[]", + "primaryKey": false, + "notNull": true, + "default": "'{80,100}'::integer[]" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_budget_member_default_idx": { + "name": "billing_budget_member_default_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_budget\".\"scope\" = 'member_default'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_budget_user_idx": { + "name": "billing_budget_user_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_budget\".\"scope\" = 'user'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_budget_project_idx": { + "name": "billing_budget_project_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_budget\".\"scope\" = 'project'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_budget_api_key_idx": { + "name": "billing_budget_api_key_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "api_key_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_budget\".\"scope\" = 'api_key'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_budget_organization_id_organization_id_fk": { + "name": "billing_budget_organization_id_organization_id_fk", + "tableFrom": "billing_budget", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_budget_user_id_user_id_fk": { + "name": "billing_budget_user_id_user_id_fk", + "tableFrom": "billing_budget", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_budget_api_key_id_apikey_id_fk": { + "name": "billing_budget_api_key_id_apikey_id_fk", + "tableFrom": "billing_budget", + "tableTo": "apikey", + "columnsFrom": [ + "api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_budget_created_by_user_id_user_id_fk": { + "name": "billing_budget_created_by_user_id_user_id_fk", + "tableFrom": "billing_budget", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "billing_budget_project_fk": { + "name": "billing_budget_project_fk", + "tableFrom": "billing_budget", + "tableTo": "project", + "columnsFrom": [ + "project_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_budget_target_check": { + "name": "billing_budget_target_check", + "value": "case \"billing_budget\".\"scope\"\n when 'member_default' then \"billing_budget\".\"user_id\" is null and \"billing_budget\".\"project_id\" is null and \"billing_budget\".\"api_key_id\" is null and \"billing_budget\".\"amount_cents\" is not null\n when 'user' then \"billing_budget\".\"user_id\" is not null and \"billing_budget\".\"project_id\" is null and \"billing_budget\".\"api_key_id\" is null\n when 'project' then \"billing_budget\".\"user_id\" is null and \"billing_budget\".\"project_id\" is not null and \"billing_budget\".\"api_key_id\" is null and \"billing_budget\".\"amount_cents\" is not null\n when 'api_key' then \"billing_budget\".\"user_id\" is null and \"billing_budget\".\"project_id\" is null and \"billing_budget\".\"api_key_id\" is not null and \"billing_budget\".\"amount_cents\" is not null\n end" + }, + "billing_budget_amount_check": { + "name": "billing_budget_amount_check", + "value": "\"billing_budget\".\"amount_cents\" is null or \"billing_budget\".\"amount_cents\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.billing_budget_hold": { + "name": "billing_budget_hold", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "billing_budget_scope", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "spent_cents": { + "name": "spent_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "limit_cents": { + "name": "limit_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_budget_hold_organization_id_organization_id_fk": { + "name": "billing_budget_hold_organization_id_organization_id_fk", + "tableFrom": "billing_budget_hold", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "billing_budget_hold_organization_id_scope_target_id_pk": { + "name": "billing_budget_hold_organization_id_scope_target_id_pk", + "columns": [ + "organization_id", + "scope", + "target_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_contract": { + "name": "billing_contract", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "billing_contract_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "billing_contract_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "terms": { + "name": "terms", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "effective_from": { + "name": "effective_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "effective_until": { + "name": "effective_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "term_end": { + "name": "term_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auto_renew": { + "name": "auto_renew", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "supersedes_id": { + "name": "supersedes_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending_invoice_id": { + "name": "pending_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending_expires_at": { + "name": "pending_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_contract_organization_idx": { + "name": "billing_contract_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effective_from", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_contract_pending_idx": { + "name": "billing_contract_pending_idx", + "columns": [ + { + "expression": "pending_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_contract\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_contract_organization_id_organization_id_fk": { + "name": "billing_contract_organization_id_organization_id_fk", + "tableFrom": "billing_contract", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_contract_account_id_billing_account_id_fk": { + "name": "billing_contract_account_id_billing_account_id_fk", + "tableFrom": "billing_contract", + "tableTo": "billing_account", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_contract_created_by_user_id_user_id_fk": { + "name": "billing_contract_created_by_user_id_user_id_fk", + "tableFrom": "billing_contract", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_counter": { + "name": "billing_counter", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_credit_entry": { + "name": "billing_credit_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "billing_credit_entry_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_credit_entry_grant_idx": { + "name": "billing_credit_entry_grant_idx", + "columns": [ + { + "expression": "grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_credit_entry_grant_id_billing_credit_grant_id_fk": { + "name": "billing_credit_entry_grant_id_billing_credit_grant_id_fk", + "tableFrom": "billing_credit_entry", + "tableTo": "billing_credit_grant", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_credit_grant": { + "name": "billing_credit_grant", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "billing_grant_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "meter": { + "name": "meter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dimension": { + "name": "dimension", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "applies_to": { + "name": "applies_to", + "type": "billing_grant_applies_to", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'usage'" + }, + "source": { + "name": "source", + "type": "billing_grant_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "grant_key": { + "name": "grant_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contract_id": { + "name": "contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "effective_at": { + "name": "effective_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "voided_at": { + "name": "voided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_credit_grant_organization_idx": { + "name": "billing_credit_grant_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "effective_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_credit_grant_invoice_idx": { + "name": "billing_credit_grant_invoice_idx", + "columns": [ + { + "expression": "invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_credit_grant\".\"invoice_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_credit_grant_organization_id_organization_id_fk": { + "name": "billing_credit_grant_organization_id_organization_id_fk", + "tableFrom": "billing_credit_grant", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_credit_grant_created_by_user_id_user_id_fk": { + "name": "billing_credit_grant_created_by_user_id_user_id_fk", + "tableFrom": "billing_credit_grant", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_credit_grant_key_unique": { + "name": "billing_credit_grant_key_unique", + "nullsNotDistinct": false, + "columns": [ + "grant_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_credit_note": { + "name": "billing_credit_note", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "refund_cents": { + "name": "refund_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "billing_credit_note_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "provider_credit_note_id": { + "name": "provider_credit_note_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sync_attempts": { + "name": "sync_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_credit_note_invoice_idx": { + "name": "billing_credit_note_invoice_idx", + "columns": [ + { + "expression": "invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_credit_note_sync_idx": { + "name": "billing_credit_note_sync_idx", + "columns": [ + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_credit_note\".\"next_sync_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_credit_note_invoice_id_billing_invoice_id_fk": { + "name": "billing_credit_note_invoice_id_billing_invoice_id_fk", + "tableFrom": "billing_credit_note", + "tableTo": "billing_invoice", + "columnsFrom": [ + "invoice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_credit_note_number_unique": { + "name": "billing_credit_note_number_unique", + "nullsNotDistinct": false, + "columns": [ + "number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_hold": { + "name": "billing_hold", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "billing_hold_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_hold_organization_id_organization_id_fk": { + "name": "billing_hold_organization_id_organization_id_fk", + "tableFrom": "billing_hold", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "billing_hold_organization_id_reason_pk": { + "name": "billing_hold_organization_id_reason_pk", + "columns": [ + "organization_id", + "reason" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_invoice": { + "name": "billing_invoice", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_slug": { + "name": "organization_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number": { + "name": "number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "billing_invoice_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "billing_invoice_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'usd'" + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "subtotal_cents": { + "name": "subtotal_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "discount_cents": { + "name": "discount_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "credit_cents": { + "name": "credit_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "tax_cents": { + "name": "tax_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "total_cents": { + "name": "total_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "amount_paid_cents": { + "name": "amount_paid_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "amount_due_cents": { + "name": "amount_due_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": "0" + }, + "collection_method": { + "name": "collection_method", + "type": "billing_collection_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "billing_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_customer_id": { + "name": "provider_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_invoice_id": { + "name": "provider_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hosted_url": { + "name": "hosted_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pdf_url": { + "name": "pdf_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_email": { + "name": "billing_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finalized_at": { + "name": "finalized_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "voided_at": { + "name": "voided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_action": { + "name": "sync_action", + "type": "billing_sync_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "sync_attempts": { + "name": "sync_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "collection_attempts": { + "name": "collection_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_collection_at": { + "name": "next_collection_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "payment_state": { + "name": "payment_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_error": { + "name": "payment_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "terms_snapshot": { + "name": "terms_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_invoice_organization_idx": { + "name": "billing_invoice_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_period_idx": { + "name": "billing_invoice_period_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_invoice\".\"kind\" = 'period' and \"billing_invoice\".\"status\" <> 'void'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_final_idx": { + "name": "billing_invoice_final_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_invoice\".\"kind\" = 'final' and \"billing_invoice\".\"status\" <> 'void'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_sync_idx": { + "name": "billing_invoice_sync_idx", + "columns": [ + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_invoice\".\"next_sync_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_collection_idx": { + "name": "billing_invoice_collection_idx", + "columns": [ + { + "expression": "next_collection_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_invoice\".\"next_collection_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_created_at_idx": { + "name": "billing_invoice_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_invoice_number_unique": { + "name": "billing_invoice_number_unique", + "nullsNotDistinct": false, + "columns": [ + "number" + ] + }, + "billing_invoice_provider_unique": { + "name": "billing_invoice_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_invoice_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_invoice_line": { + "name": "billing_invoice_line", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "billing_line_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meter": { + "name": "meter", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dimension": { + "name": "dimension", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "covered_units": { + "name": "covered_units", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "unit_nanos": { + "name": "unit_nanos", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "contract_id": { + "name": "contract_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_id": { + "name": "grant_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "charge_key": { + "name": "charge_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_invoice_line_invoice_idx": { + "name": "billing_invoice_line_invoice_idx", + "columns": [ + { + "expression": "invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_line_charge_key_idx": { + "name": "billing_invoice_line_charge_key_idx", + "columns": [ + { + "expression": "charge_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_invoice_line\".\"charge_key\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_invoice_line_contract_idx": { + "name": "billing_invoice_line_contract_idx", + "columns": [ + { + "expression": "contract_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_invoice_line\".\"contract_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_invoice_line_invoice_id_billing_invoice_id_fk": { + "name": "billing_invoice_line_invoice_id_billing_invoice_id_fk", + "tableFrom": "billing_invoice_line", + "tableTo": "billing_invoice", + "columnsFrom": [ + "invoice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_notification": { + "name": "billing_notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "billing_notification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recipients": { + "name": "recipients", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_notification_next_attempt_idx": { + "name": "billing_notification_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_notification\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_notification_created_at_idx": { + "name": "billing_notification_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_notification_organization_id_organization_id_fk": { + "name": "billing_notification_organization_id_organization_id_fk", + "tableFrom": "billing_notification", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_notification_user_id_user_id_fk": { + "name": "billing_notification_user_id_user_id_fk", + "tableFrom": "billing_notification", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_notification_dedupe_unique": { + "name": "billing_notification_dedupe_unique", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_payment": { + "name": "billing_payment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_payment_id": { + "name": "provider_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "recorded_by_user_id": { + "name": "recorded_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_payment_invoice_idx": { + "name": "billing_payment_invoice_idx", + "columns": [ + { + "expression": "invoice_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_payment_invoice_id_billing_invoice_id_fk": { + "name": "billing_payment_invoice_id_billing_invoice_id_fk", + "tableFrom": "billing_payment", + "tableTo": "billing_invoice", + "columnsFrom": [ + "invoice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_payment_provider_unique": { + "name": "billing_payment_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "provider_payment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_period": { + "name": "billing_period", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invoice_id": { + "name": "invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage": { + "name": "usage", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "warnings": { + "name": "warnings", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_period_account_start_idx": { + "name": "billing_period_account_start_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_period_organization_id_organization_id_fk": { + "name": "billing_period_organization_id_organization_id_fk", + "tableFrom": "billing_period", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_period_account_id_billing_account_id_fk": { + "name": "billing_period_account_id_billing_account_id_fk", + "tableFrom": "billing_period", + "tableTo": "billing_account", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_provider_event": { + "name": "billing_provider_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "billing_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider_event_id": { + "name": "provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "billing_provider_event_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_provider_event_unique_idx": { + "name": "billing_provider_event_unique_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_provider_event_next_attempt_idx": { + "name": "billing_provider_event_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"billing_provider_event\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_provider_event_created_at_idx": { + "name": "billing_provider_event_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_quota": { + "name": "billing_quota", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meter": { + "name": "meter", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reserved": { + "name": "reserved", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "billing_quota_organization_id_organization_id_fk": { + "name": "billing_quota_organization_id_organization_id_fk", + "tableFrom": "billing_quota", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "billing_quota_organization_id_meter_period_start_pk": { + "name": "billing_quota_organization_id_meter_period_start_pk", + "columns": [ + "organization_id", + "meter", + "period_start" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_usage": { + "name": "billing_usage", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hour_start": { + "name": "hour_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "meter": { + "name": "meter", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dimension": { + "name": "dimension", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shard": { + "name": "shard", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "quantity": { + "name": "quantity", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "billing_usage_hour_idx": { + "name": "billing_usage_hour_idx", + "columns": [ + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_usage_organization_id_organization_id_fk": { + "name": "billing_usage_organization_id_organization_id_fk", + "tableFrom": "billing_usage", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "billing_usage_organization_id_hour_start_meter_dimension_environment_id_shard_pk": { + "name": "billing_usage_organization_id_hour_start_meter_dimension_environment_id_shard_pk", + "columns": [ + "organization_id", + "hour_start", + "meter", + "dimension", + "environment_id", + "shard" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_usage_attribution": { + "name": "billing_usage_attribution", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hour_start": { + "name": "hour_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "meter": { + "name": "meter", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dimension": { + "name": "dimension", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "shard": { + "name": "shard", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "quantity": { + "name": "quantity", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "billing_usage_attribution_hour_idx": { + "name": "billing_usage_attribution_hour_idx", + "columns": [ + { + "expression": "hour_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_usage_attribution_organization_id_organization_id_fk": { + "name": "billing_usage_attribution_organization_id_organization_id_fk", + "tableFrom": "billing_usage_attribution", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "billing_usage_attribution_pk": { + "name": "billing_usage_attribution_pk", + "columns": [ + "organization_id", + "hour_start", + "meter", + "dimension", + "environment_id", + "api_key_id", + "user_id", + "shard" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.admin_audit_event": { + "name": "admin_audit_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "admin_audit_event_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_email": { + "name": "actor_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "admin_audit_event_target_idx": { + "name": "admin_audit_event_target_idx", + "columns": [ + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_audit_event_organization_idx": { + "name": "admin_audit_event_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"admin_audit_event\".\"organization_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "admin_audit_event_created_at_idx": { + "name": "admin_audit_event_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audience": { + "name": "audience", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "audience_environment_fk": { + "name": "audience_environment_fk", + "tableFrom": "audience", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "audience_environment_name_unique": { + "name": "audience_environment_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "name" + ] + }, + "audience_id_organization_unique": { + "name": "audience_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.audience_member": { + "name": "audience_member", + "schema": "", + "columns": { + "audience_id": { + "name": "audience_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "audience_member_contact_idx": { + "name": "audience_member_contact_idx", + "columns": [ + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "audience_member_audience_fk": { + "name": "audience_member_audience_fk", + "tableFrom": "audience_member", + "tableTo": "audience", + "columnsFrom": [ + "audience_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "audience_member_contact_fk": { + "name": "audience_member_contact_fk", + "tableFrom": "audience_member", + "tableTo": "contact", + "columnsFrom": [ + "contact_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "audience_member_audience_id_contact_id_pk": { + "name": "audience_member_audience_id_contact_id_pk", + "columns": [ + "audience_id", + "contact_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.auth_email": { + "name": "auth_email", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "auth_email_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "to": { + "name": "to", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "auth_email_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "auth_email_next_attempt_idx": { + "name": "auth_email_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"auth_email\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.broadcast": { + "name": "broadcast", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_owner_id": { + "name": "api_key_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "broadcast_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "audience_id": { + "name": "audience_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "segment_id": { + "name": "segment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from": { + "name": "from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reply_to": { + "name": "reply_to", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "preview_text": { + "name": "preview_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "html": { + "name": "html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "track_opens": { + "name": "track_opens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "track_clicks": { + "name": "track_clicks", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rate_per_second": { + "name": "rate_per_second", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "contact_upper_bound": { + "name": "contact_upper_bound", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recipients_estimated": { + "name": "recipients_estimated", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "materialized": { + "name": "materialized", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "screening_status": { + "name": "screening_status", + "type": "broadcast_screening_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "screening_held_at": { + "name": "screening_held_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "broadcast_environment_created_at_idx": { + "name": "broadcast_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "broadcast_due_idx": { + "name": "broadcast_due_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"broadcast\".\"status\" in ('scheduled', 'sending')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "broadcast_environment_fk": { + "name": "broadcast_environment_fk", + "tableFrom": "broadcast", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "broadcast_id_organization_unique": { + "name": "broadcast_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "broadcast_environment_idempotency_key_unique": { + "name": "broadcast_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "broadcast_target_check": { + "name": "broadcast_target_check", + "value": "(\"broadcast\".\"audience_id\" is null) <> (\"broadcast\".\"segment_id\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.broadcast_stat": { + "name": "broadcast_stat", + "schema": "", + "columns": { + "broadcast_id": { + "name": "broadcast_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "shard": { + "name": "shard", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipients": { + "name": "recipients", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sent": { + "name": "sent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "delivered": { + "name": "delivered", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bounced": { + "name": "bounced", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "complained": { + "name": "complained", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed": { + "name": "failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "opened": { + "name": "opened", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "clicked": { + "name": "clicked", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "unsubscribed": { + "name": "unsubscribed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": { + "broadcast_stat_broadcast_fk": { + "name": "broadcast_stat_broadcast_fk", + "tableFrom": "broadcast_stat", + "tableTo": "broadcast", + "columnsFrom": [ + "broadcast_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "broadcast_stat_broadcast_id_shard_pk": { + "name": "broadcast_stat_broadcast_id_shard_pk", + "columns": [ + "broadcast_id", + "shard" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact": { + "name": "contact", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "contact_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "contact_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'subscribed'" + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "source": { + "name": "source", + "type": "contact_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subscribed_at": { + "name": "subscribed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "unsubscribed_at": { + "name": "unsubscribed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pending_since": { + "name": "pending_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmation_sent_at": { + "name": "confirmation_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmation_count": { + "name": "confirmation_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_environment_sequence_idx": { + "name": "contact_environment_sequence_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_environment_id_idx": { + "name": "contact_environment_id_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_properties_idx": { + "name": "contact_properties_idx", + "columns": [ + { + "expression": "properties", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "jsonb_path_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "contact_pending_since_idx": { + "name": "contact_pending_since_idx", + "columns": [ + { + "expression": "pending_since", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"contact\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_environment_fk": { + "name": "contact_environment_fk", + "tableFrom": "contact", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contact_environment_address_unique": { + "name": "contact_environment_address_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "address" + ] + }, + "contact_id_organization_unique": { + "name": "contact_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_counter": { + "name": "contact_counter", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "contact_counter_environment_fk": { + "name": "contact_counter_environment_fk", + "tableFrom": "contact_counter", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "contact_counter_count_check": { + "name": "contact_counter_count_check", + "value": "\"contact_counter\".\"count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.contact_property": { + "name": "contact_property", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "contact_property_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "fallback": { + "name": "fallback", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "contact_property_environment_fk": { + "name": "contact_property_environment_fk", + "tableFrom": "contact_property", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contact_property_environment_key_unique": { + "name": "contact_property_environment_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_topic": { + "name": "contact_topic", + "schema": "", + "columns": { + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "topic_id": { + "name": "topic_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "contact_topic_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "contact_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contact_topic_topic_status_idx": { + "name": "contact_topic_topic_status_idx", + "columns": [ + { + "expression": "topic_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_topic_contact_fk": { + "name": "contact_topic_contact_fk", + "tableFrom": "contact_topic", + "tableTo": "contact", + "columnsFrom": [ + "contact_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_topic_topic_fk": { + "name": "contact_topic_topic_fk", + "tableFrom": "contact_topic", + "tableTo": "topic", + "columnsFrom": [ + "topic_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contact_topic_contact_id_topic_id_pk": { + "name": "contact_topic_contact_id_topic_id_pk", + "columns": [ + "contact_id", + "topic_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_screening": { + "name": "content_screening", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "classifier": { + "name": "classifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verdict": { + "name": "verdict", + "type": "content_screening_verdict", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "enforced": { + "name": "enforced", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "scores": { + "name": "scores", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "top_category": { + "name": "top_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "top_score": { + "name": "top_score", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "latency_ms": { + "name": "latency_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "content_screening_organization_created_at_idx": { + "name": "content_screening_organization_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_screening_created_at_idx": { + "name": "content_screening_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "content_screening_subject_idx": { + "name": "content_screening_subject_idx", + "columns": [ + { + "expression": "product", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "content_screening_environment_fk": { + "name": "content_screening_environment_fk", + "tableFrom": "content_screening", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.content_screening_counter": { + "name": "content_screening_counter", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "seen": { + "name": "seen", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "owed": { + "name": "owed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "content_screening_counter_organization_id_organization_id_fk": { + "name": "content_screening_counter_organization_id_organization_id_fk", + "tableFrom": "content_screening_counter", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "content_screening_counter_organization_id_product_group_id_pk": { + "name": "content_screening_counter_organization_id_product_group_id_pk", + "columns": [ + "organization_id", + "product", + "group_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dedicated_ip_delivery_rollup": { + "name": "dedicated_ip_delivery_rollup", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hour": { + "name": "hour", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "sending_ip": { + "name": "sending_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivered": { + "name": "delivered", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "dedicated_ip_delivery_rollup_hour_idx": { + "name": "dedicated_ip_delivery_rollup_hour_idx", + "columns": [ + { + "expression": "hour", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dedicated_ip_delivery_rollup_organization_id_organization_id_fk": { + "name": "dedicated_ip_delivery_rollup_organization_id_organization_id_fk", + "tableFrom": "dedicated_ip_delivery_rollup", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dedicated_ip_delivery_rollup_organization_id_hour_sending_ip_pk": { + "name": "dedicated_ip_delivery_rollup_organization_id_hour_sending_ip_pk", + "columns": [ + "organization_id", + "hour", + "sending_ip" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dedicated_ip_pool": { + "name": "dedicated_ip_pool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "dedicated_ip_pool_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "ips": { + "name": "ips", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "dedicated_share": { + "name": "dedicated_share", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "status_message": { + "name": "status_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provisioned_at": { + "name": "provisioned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ready_at": { + "name": "ready_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "release_after": { + "name": "release_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "release_started_at": { + "name": "release_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tls_required_at": { + "name": "tls_required_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "marketing_configured_at": { + "name": "marketing_configured_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dedicated_ip_pool_next_check_at_idx": { + "name": "dedicated_ip_pool_next_check_at_idx", + "columns": [ + { + "expression": "next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"dedicated_ip_pool\".\"next_check_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dedicated_ip_pool_organization_id_organization_id_fk": { + "name": "dedicated_ip_pool_organization_id_organization_id_fk", + "tableFrom": "dedicated_ip_pool", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dedicated_ip_pool_organization_unique": { + "name": "dedicated_ip_pool_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dmarc_notification": { + "name": "dmarc_notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "dmarc_notification_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "policy_changed_at": { + "name": "policy_changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "detail": { + "name": "detail", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "dmarc_notification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recipients": { + "name": "recipients", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dmarc_notification_due_idx": { + "name": "dmarc_notification_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"dmarc_notification\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "dmarc_notification_created_at_idx": { + "name": "dmarc_notification_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dmarc_notification_domain_fk": { + "name": "dmarc_notification_domain_fk", + "tableFrom": "dmarc_notification", + "tableTo": "domain", + "columnsFrom": [ + "domain_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dmarc_notification_dedupe_unique": { + "name": "dmarc_notification_dedupe_unique", + "nullsNotDistinct": false, + "columns": [ + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dmarc_report": { + "name": "dmarc_report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reporter_org": { + "name": "reporter_org", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reporter_email": { + "name": "reporter_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "report_id": { + "name": "report_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_begin": { + "name": "period_begin", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "policy_published": { + "name": "policy_published", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "message_count": { + "name": "message_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pass_count": { + "name": "pass_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "dmarc_report_domain_period_idx": { + "name": "dmarc_report_domain_period_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dmarc_report_domain_id_idx": { + "name": "dmarc_report_domain_id_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dmarc_report_created_at_idx": { + "name": "dmarc_report_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dmarc_report_organization_idx": { + "name": "dmarc_report_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dmarc_report_domain_fk": { + "name": "dmarc_report_domain_fk", + "tableFrom": "dmarc_report", + "tableTo": "domain", + "columnsFrom": [ + "domain_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "dmarc_report_reporter_unique": { + "name": "dmarc_report_reporter_unique", + "nullsNotDistinct": false, + "columns": [ + "domain_id", + "reporter_org", + "report_id" + ] + }, + "dmarc_report_id_organization_unique": { + "name": "dmarc_report_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.dmarc_report_record": { + "name": "dmarc_report_record", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dmarc_report_id": { + "name": "dmarc_report_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_begin": { + "name": "period_begin", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_end": { + "name": "period_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "source_ip": { + "name": "source_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dkim": { + "name": "dkim", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "spf": { + "name": "spf", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dmarc_pass": { + "name": "dmarc_pass", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "header_from": { + "name": "header_from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "envelope_from": { + "name": "envelope_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_results": { + "name": "auth_results", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "via_coss": { + "name": "via_coss", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "dmarc_report_record_report_idx": { + "name": "dmarc_report_record_report_idx", + "columns": [ + { + "expression": "dmarc_report_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dmarc_report_record_domain_period_idx": { + "name": "dmarc_report_record_domain_period_idx", + "columns": [ + { + "expression": "domain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "dmarc_report_record_organization_idx": { + "name": "dmarc_report_record_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "dmarc_report_record_report_fk": { + "name": "dmarc_report_record_report_fk", + "tableFrom": "dmarc_report_record", + "tableTo": "dmarc_report", + "columnsFrom": [ + "dmarc_report_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain": { + "name": "domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "domain_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "can_send": { + "name": "can_send", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dkim_tokens": { + "name": "dkim_tokens", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "dkim_hosted_zone": { + "name": "dkim_hosted_zone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mail_from_domain": { + "name": "mail_from_domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mail_from_status": { + "name": "mail_from_status", + "type": "mail_from_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "records": { + "name": "records", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "bimi": { + "name": "bimi", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verification_started_at": { + "name": "verification_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failed_at": { + "name": "failed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dmarc_report_token": { + "name": "dmarc_report_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dmarc_record": { + "name": "dmarc_record", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dmarc_inherited_record": { + "name": "dmarc_inherited_record", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dmarc_policy_changed_at": { + "name": "dmarc_policy_changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subaccount_id": { + "name": "subaccount_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_with_subaccounts": { + "name": "shared_with_subaccounts", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "domain_subaccount_idx": { + "name": "domain_subaccount_idx", + "columns": [ + { + "expression": "subaccount_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"domain\".\"subaccount_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_name_active_unique": { + "name": "domain_name_active_unique", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"domain\".\"status\" <> 'failed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_organization_idx": { + "name": "domain_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_next_check_at_idx": { + "name": "domain_next_check_at_idx", + "columns": [ + { + "expression": "next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"domain\".\"next_check_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "domain_organization_id_organization_id_fk": { + "name": "domain_organization_id_organization_id_fk", + "tableFrom": "domain", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "domain_created_by_user_id_user_id_fk": { + "name": "domain_created_by_user_id_user_id_fk", + "tableFrom": "domain", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "domain_subaccount_fk": { + "name": "domain_subaccount_fk", + "tableFrom": "domain", + "tableTo": "email_subaccount", + "columnsFrom": [ + "subaccount_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "domain_dmarc_report_token_unique": { + "name": "domain_dmarc_report_token_unique", + "nullsNotDistinct": false, + "columns": [ + "dmarc_report_token" + ] + }, + "domain_id_organization_unique": { + "name": "domain_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "domain_shared_not_subaccount": { + "name": "domain_shared_not_subaccount", + "value": "not (\"domain\".\"shared_with_subaccounts\" and \"domain\".\"subaccount_id\" is not null)" + } + }, + "isRLSEnabled": false + }, + "public.email": { + "name": "email", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_owner_id": { + "name": "api_key_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "email_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "from": { + "name": "from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to": { + "name": "to", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "cc": { + "name": "cc", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "bcc": { + "name": "bcc", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "reply_to": { + "name": "reply_to", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "html": { + "name": "html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "discard_body": { + "name": "discard_body", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "screening_exempt": { + "name": "screening_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "store_content": { + "name": "store_content", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_discarded_at": { + "name": "content_discarded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream": { + "name": "stream", + "type": "email_stream", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'transactional'" + }, + "track_opens": { + "name": "track_opens", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "track_clicks": { + "name": "track_clicks", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "unsubscribe": { + "name": "unsubscribe", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "tracking_token": { + "name": "tracking_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "open_count": { + "name": "open_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_opened_at": { + "name": "first_opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_opened_at": { + "name": "last_opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "click_count": { + "name": "click_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_clicked_at": { + "name": "first_clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_clicked_at": { + "name": "last_clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "batch_id": { + "name": "batch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "batch_index": { + "name": "batch_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "broadcast_id": { + "name": "broadcast_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "billable_recipients": { + "name": "billable_recipients", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template_version": { + "name": "template_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "template_locale": { + "name": "template_locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dedicated_pool": { + "name": "dedicated_pool", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "require_tls": { + "name": "require_tls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hipaa_origin": { + "name": "hipaa_origin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "subaccount_id": { + "name": "subaccount_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subaccount_tenant": { + "name": "subaccount_tenant", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "traceparent": { + "name": "traceparent", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "email_batch_idx": { + "name": "email_batch_idx", + "columns": [ + { + "expression": "batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "batch_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email\".\"batch_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_broadcast_contact_unique": { + "name": "email_broadcast_contact_unique", + "columns": [ + { + "expression": "broadcast_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"email\".\"broadcast_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_broadcast_status_idx": { + "name": "email_broadcast_status_idx", + "columns": [ + { + "expression": "broadcast_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email\".\"broadcast_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_environment_created_at_idx": { + "name": "email_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_organization_created_at_idx": { + "name": "email_organization_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_organization_send_time_idx": { + "name": "email_organization_send_time_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"sent_at\", \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_subaccount_send_time_idx": { + "name": "email_subaccount_send_time_idx", + "columns": [ + { + "expression": "subaccount_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"sent_at\", \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email\".\"subaccount_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_scheduled_due_idx": { + "name": "email_scheduled_due_idx", + "columns": [ + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email\".\"status\" = 'scheduled'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_queued_created_at_idx": { + "name": "email_queued_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_created_at_idx": { + "name": "email_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_environment_fk": { + "name": "email_environment_fk", + "tableFrom": "email", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_batch_fk": { + "name": "email_batch_fk", + "tableFrom": "email", + "tableTo": "email_batch", + "columnsFrom": [ + "batch_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_tracking_token_unique": { + "name": "email_tracking_token_unique", + "nullsNotDistinct": false, + "columns": [ + "tracking_token" + ] + }, + "email_environment_idempotency_key_unique": { + "name": "email_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + }, + "email_id_organization_unique": { + "name": "email_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_attachment": { + "name": "email_attachment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_attachment_email_fk": { + "name": "email_attachment_email_fk", + "tableFrom": "email_attachment", + "tableTo": "email", + "columnsFrom": [ + "email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_attachment_email_position_unique": { + "name": "email_attachment_email_position_unique", + "nullsNotDistinct": false, + "columns": [ + "email_id", + "position" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_batch": { + "name": "email_batch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_batch_environment_fk": { + "name": "email_batch_environment_fk", + "tableFrom": "email_batch", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_batch_environment_idempotency_key_unique": { + "name": "email_batch_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + }, + "email_batch_id_organization_unique": { + "name": "email_batch_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_event": { + "name": "email_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "email_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_event_type": { + "name": "provider_event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipient": { + "name": "recipient", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_event_email_occurred_at_idx": { + "name": "email_event_email_occurred_at_idx", + "columns": [ + { + "expression": "email_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_event_email_fk": { + "name": "email_event_email_fk", + "tableFrom": "email_event", + "tableTo": "email", + "columnsFrom": [ + "email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_event_email_dedupe_unique": { + "name": "email_event_email_dedupe_unique", + "nullsNotDistinct": false, + "columns": [ + "email_id", + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_link": { + "name": "email_link", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "click_count": { + "name": "click_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_clicked_at": { + "name": "first_clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_clicked_at": { + "name": "last_clicked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_link_email_fk": { + "name": "email_link_email_fk", + "tableFrom": "email_link", + "tableTo": "email", + "columnsFrom": [ + "email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_link_email_position_unique": { + "name": "email_link_email_position_unique", + "nullsNotDistinct": false, + "columns": [ + "email_id", + "position" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_recipient": { + "name": "email_recipient", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "email_recipient_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "email_recipient_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "last_event_at": { + "name": "last_event_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_recipient_organization_address_idx": { + "name": "email_recipient_organization_address_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "address", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_recipient_email_fk": { + "name": "email_recipient_email_fk", + "tableFrom": "email_recipient", + "tableTo": "email", + "columnsFrom": [ + "email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_recipient_email_address_unique": { + "name": "email_recipient_email_address_unique", + "nullsNotDistinct": false, + "columns": [ + "email_id", + "address" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_reputation_rollup": { + "name": "email_reputation_rollup", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket_start": { + "name": "bucket_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "email_stream", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'transactional'" + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "computed_revision": { + "name": "computed_revision", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "recipients": { + "name": "recipients", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "bounces": { + "name": "bounces", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "complaints": { + "name": "complaints", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "simulator_recipients": { + "name": "simulator_recipients", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "simulator_bounces": { + "name": "simulator_bounces", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "simulator_complaints": { + "name": "simulator_complaints", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_reputation_rollup_organization_id_organization_id_fk": { + "name": "email_reputation_rollup_organization_id_organization_id_fk", + "tableFrom": "email_reputation_rollup", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "email_reputation_rollup_organization_id_bucket_start_stream_pk": { + "name": "email_reputation_rollup_organization_id_bucket_start_stream_pk", + "columns": [ + "organization_id", + "bucket_start", + "stream" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_sender_domain_block": { + "name": "email_sender_domain_block", + "schema": "", + "columns": { + "domain": { + "name": "domain", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_id": { + "name": "group_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_sender_domain_block_organization_idx": { + "name": "email_sender_domain_block_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_storage_settings": { + "name": "email_storage_settings", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "store_content": { + "name": "store_content", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_storage_settings_environment_fk": { + "name": "email_storage_settings_environment_fk", + "tableFrom": "email_storage_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_subaccount": { + "name": "email_subaccount", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "email_subaccount_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "daily_recipient_limit": { + "name": "daily_recipient_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "per_second_recipient_limit": { + "name": "per_second_recipient_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "per_second_burst": { + "name": "per_second_burst", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reputation_status": { + "name": "reputation_status", + "type": "email_subaccount_reputation", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'good'" + }, + "reputation_trigger": { + "name": "reputation_trigger", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reputation_changed_at": { + "name": "reputation_changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reinstated_at": { + "name": "reinstated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_evaluation_at": { + "name": "next_evaluation_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evaluation_lease_until": { + "name": "evaluation_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deletion_lease_until": { + "name": "deletion_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "deletion_attempts": { + "name": "deletion_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_subaccount_environment_created_at_idx": { + "name": "email_subaccount_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_subaccount_next_evaluation_idx": { + "name": "email_subaccount_next_evaluation_idx", + "columns": [ + { + "expression": "next_evaluation_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email_subaccount\".\"next_evaluation_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_subaccount_deleting_idx": { + "name": "email_subaccount_deleting_idx", + "columns": [ + { + "expression": "deletion_lease_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email_subaccount\".\"status\" = 'deleting'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_subaccount_environment_fk": { + "name": "email_subaccount_environment_fk", + "tableFrom": "email_subaccount", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_subaccount_environment_uid_unique": { + "name": "email_subaccount_environment_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "uid" + ] + }, + "email_subaccount_id_organization_unique": { + "name": "email_subaccount_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_suppression": { + "name": "email_suppression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "email_suppression_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "email_suppression_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subaccount_id": { + "name": "subaccount_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_suppression_subaccount_sequence_idx": { + "name": "email_suppression_subaccount_sequence_idx", + "columns": [ + { + "expression": "subaccount_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"email_suppression\".\"subaccount_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_suppression_environment_sequence_idx": { + "name": "email_suppression_environment_sequence_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_suppression_environment_fk": { + "name": "email_suppression_environment_fk", + "tableFrom": "email_suppression", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_suppression_subaccount_fk": { + "name": "email_suppression_subaccount_fk", + "tableFrom": "email_suppression", + "tableTo": "email_subaccount", + "columnsFrom": [ + "subaccount_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_suppression_environment_subaccount_address_scope_unique": { + "name": "email_suppression_environment_subaccount_address_scope_unique", + "nullsNotDistinct": true, + "columns": [ + "environment_id", + "subaccount_id", + "address", + "scope" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_template": { + "name": "email_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_version": { + "name": "published_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "email_template_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + } + }, + "indexes": { + "email_template_environment_created_at_idx": { + "name": "email_template_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_template_environment_fk": { + "name": "email_template_environment_fk", + "tableFrom": "email_template", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_template_environment_alias_unique": { + "name": "email_template_environment_alias_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "alias" + ] + }, + "email_template_id_organization_unique": { + "name": "email_template_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_template_content": { + "name": "email_template_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "html": { + "name": "html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_template_content_version_fk": { + "name": "email_template_content_version_fk", + "tableFrom": "email_template_content", + "tableTo": "email_template_version", + "columnsFrom": [ + "version_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_template_content_version_locale_unique": { + "name": "email_template_content_version_locale_unique", + "nullsNotDistinct": false, + "columns": [ + "version_id", + "locale" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_template_version": { + "name": "email_template_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "default_locale": { + "name": "default_locale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "sample_variables": { + "name": "sample_variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "has_unpublished_changes": { + "name": "has_unpublished_changes", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "email_template_version_draft_unique": { + "name": "email_template_version_draft_unique", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"email_template_version\".\"version\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_template_version_template_fk": { + "name": "email_template_version_template_fk", + "tableFrom": "email_template_version", + "tableTo": "email_template", + "columnsFrom": [ + "template_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_template_version_template_version_unique": { + "name": "email_template_version_template_version_unique", + "nullsNotDistinct": false, + "columns": [ + "template_id", + "version" + ] + }, + "email_template_version_id_organization_unique": { + "name": "email_template_version_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_tenant": { + "name": "email_tenant", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subaccount_id": { + "name": "subaccount_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stream": { + "name": "stream", + "type": "email_stream", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'transactional'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tenant_name": { + "name": "tenant_name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tenant_arn": { + "name": "tenant_arn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sending_status": { + "name": "sending_status", + "type": "tenant_sending_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'enabled'" + }, + "suppression_migrated_at": { + "name": "suppression_migrated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status_cause": { + "name": "status_cause", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_updated_at": { + "name": "status_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_tenant_organization_id_organization_id_fk": { + "name": "email_tenant_organization_id_organization_id_fk", + "tableFrom": "email_tenant", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_tenant_subaccount_fk": { + "name": "email_tenant_subaccount_fk", + "tableFrom": "email_tenant", + "tableTo": "email_subaccount", + "columnsFrom": [ + "subaccount_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_tenant_owner_stream_unique": { + "name": "email_tenant_owner_stream_unique", + "nullsNotDistinct": true, + "columns": [ + "organization_id", + "subaccount_id", + "stream" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_tracking_rule": { + "name": "email_tracking_rule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "opens": { + "name": "opens", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "clicks": { + "name": "clicks", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "unsubscribe": { + "name": "unsubscribe", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_tracking_rule_environment_fk": { + "name": "email_tracking_rule_environment_fk", + "tableFrom": "email_tracking_rule", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_tracking_rule_environment_category_unique": { + "name": "email_tracking_rule_environment_category_unique", + "nullsNotDistinct": true, + "columns": [ + "environment_id", + "category" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "environment_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "personal_keys": { + "name": "personal_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_organization_idx": { + "name": "environment_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_project_fk": { + "name": "environment_project_fk", + "tableFrom": "environment", + "tableTo": "project", + "columnsFrom": [ + "project_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_project_slug_unique": { + "name": "environment_project_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "slug" + ] + }, + "environment_id_organization_unique": { + "name": "environment_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_api_key": { + "name": "environment_api_key", + "schema": "", + "columns": { + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access": { + "name": "access", + "type": "api_key_access", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'full'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_api_key_environment_idx": { + "name": "environment_api_key_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_api_key_owner_idx": { + "name": "environment_api_key_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_api_key_api_key_id_apikey_id_fk": { + "name": "environment_api_key_api_key_id_apikey_id_fk", + "tableFrom": "environment_api_key", + "tableTo": "apikey", + "columnsFrom": [ + "api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_api_key_environment_id_environment_id_fk": { + "name": "environment_api_key_environment_id_environment_id_fk", + "tableFrom": "environment_api_key", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_api_key_created_by_user_id_user_id_fk": { + "name": "environment_api_key_created_by_user_id_user_id_fk", + "tableFrom": "environment_api_key", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_api_key_owner_user_id_user_id_fk": { + "name": "environment_api_key_owner_user_id_user_id_fk", + "tableFrom": "environment_api_key", + "tableTo": "user", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_inbound_address": { + "name": "environment_inbound_address", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_inbound_address_environment_fk": { + "name": "environment_inbound_address_environment_fk", + "tableFrom": "environment_inbound_address", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_inbound_address_label_unique": { + "name": "environment_inbound_address_label_unique", + "nullsNotDistinct": false, + "columns": [ + "label" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hipaa_session_activity": { + "name": "hipaa_session_activity", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "hipaa_session_activity_session_id_session_id_fk": { + "name": "hipaa_session_activity_session_id_session_id_fk", + "tableFrom": "hipaa_session_activity", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_application": { + "name": "hook_application", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hook_application_environment_created_at_idx": { + "name": "hook_application_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hook_application_environment_fk": { + "name": "hook_application_environment_fk", + "tableFrom": "hook_application", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hook_application_environment_uid_unique": { + "name": "hook_application_environment_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "uid" + ] + }, + "hook_application_id_organization_unique": { + "name": "hook_application_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_attempt": { + "name": "hook_attempt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_created_at": { + "name": "message_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_excerpt": { + "name": "response_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "hook_attempt_delivery_idx": { + "name": "hook_attempt_delivery_idx", + "columns": [ + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hook_attempt_environment_idx": { + "name": "hook_attempt_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "hook_attempt_pkey": { + "name": "hook_attempt_pkey", + "columns": [ + "message_created_at", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_delivery": { + "name": "hook_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_created_at": { + "name": "message_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "hook_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_status_code": { + "name": "last_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hook_delivery_due_idx": { + "name": "hook_delivery_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"hook_delivery\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "hook_delivery_endpoint_idx": { + "name": "hook_delivery_endpoint_idx", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hook_delivery_environment_idx": { + "name": "hook_delivery_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "message_created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "hook_delivery_pkey": { + "name": "hook_delivery_pkey", + "columns": [ + "message_created_at", + "id" + ] + } + }, + "uniqueConstraints": { + "hook_delivery_message_endpoint_unique": { + "name": "hook_delivery_message_endpoint_unique", + "nullsNotDistinct": false, + "columns": [ + "message_created_at", + "message_id", + "endpoint_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_delivery_rollup": { + "name": "hook_delivery_rollup", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hour": { + "name": "hour", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "succeeded": { + "name": "succeeded", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failed": { + "name": "failed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": { + "hook_delivery_rollup_hour_idx": { + "name": "hook_delivery_rollup_hour_idx", + "columns": [ + { + "expression": "hour", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hook_delivery_rollup_organization_id_organization_id_fk": { + "name": "hook_delivery_rollup_organization_id_organization_id_fk", + "tableFrom": "hook_delivery_rollup", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "hook_delivery_rollup_pkey": { + "name": "hook_delivery_rollup_pkey", + "columns": [ + "organization_id", + "hour" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_endpoint": { + "name": "hook_endpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filter_types": { + "name": "filter_types", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "secret_ciphertext": { + "name": "secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previous_secret_ciphertext": { + "name": "previous_secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_secret_expires_at": { + "name": "previous_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rate_limit_per_second": { + "name": "rate_limit_per_second", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failing_since": { + "name": "failing_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hook_endpoint_application_idx": { + "name": "hook_endpoint_application_idx", + "columns": [ + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hook_endpoint_application_fk": { + "name": "hook_endpoint_application_fk", + "tableFrom": "hook_endpoint", + "tableTo": "hook_application", + "columnsFrom": [ + "application_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hook_endpoint_id_organization_unique": { + "name": "hook_endpoint_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "hook_endpoint_previous_secret_check": { + "name": "hook_endpoint_previous_secret_check", + "value": "(\"hook_endpoint\".\"previous_secret_ciphertext\" is null) = (\"hook_endpoint\".\"previous_secret_expires_at\" is null)" + }, + "hook_endpoint_disabled_check": { + "name": "hook_endpoint_disabled_check", + "value": "\"hook_endpoint\".\"enabled\" = (\"hook_endpoint\".\"disabled_reason\" is null) and (\"hook_endpoint\".\"disabled_reason\" is null) = (\"hook_endpoint\".\"disabled_at\" is null)" + }, + "hook_endpoint_rate_limit_check": { + "name": "hook_endpoint_rate_limit_check", + "value": "\"hook_endpoint\".\"rate_limit_per_second\" is null or \"hook_endpoint\".\"rate_limit_per_second\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.hook_endpoint_recovery": { + "name": "hook_endpoint_recovery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "since": { + "name": "since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "until": { + "name": "until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "hook_recovery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "cursor_created_at": { + "name": "cursor_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cursor_id": { + "name": "cursor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requeued": { + "name": "requeued", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hook_endpoint_recovery_pending_idx": { + "name": "hook_endpoint_recovery_pending_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"hook_endpoint_recovery\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "hook_endpoint_recovery_endpoint_idx": { + "name": "hook_endpoint_recovery_endpoint_idx", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hook_endpoint_recovery_endpoint_fk": { + "name": "hook_endpoint_recovery_endpoint_fk", + "tableFrom": "hook_endpoint_recovery", + "tableTo": "hook_endpoint", + "columnsFrom": [ + "endpoint_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_event_type": { + "name": "hook_event_type", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema": { + "name": "schema", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "examples": { + "name": "examples", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "hook_event_type_environment_fk": { + "name": "hook_event_type_environment_fk", + "tableFrom": "hook_event_type", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hook_event_type_environment_name_unique": { + "name": "hook_event_type_environment_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_idempotency_key": { + "name": "hook_idempotency_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_created_at": { + "name": "message_created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hook_idempotency_key_created_at_idx": { + "name": "hook_idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hook_idempotency_key_environment_fk": { + "name": "hook_idempotency_key_environment_fk", + "tableFrom": "hook_idempotency_key", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hook_idempotency_key_environment_key_unique": { + "name": "hook_idempotency_key_environment_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_message": { + "name": "hook_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "payload_bytes": { + "name": "payload_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "only_endpoint_id": { + "name": "only_endpoint_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "hook_message_application_created_at_idx": { + "name": "hook_message_application_created_at_idx", + "columns": [ + { + "expression": "application_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hook_message_environment_created_at_idx": { + "name": "hook_message_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "hook_message_pkey": { + "name": "hook_message_pkey", + "columns": [ + "created_at", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_portal_session": { + "name": "hook_portal_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "application_id": { + "name": "application_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_only": { + "name": "read_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hook_portal_session_expires_at_idx": { + "name": "hook_portal_session_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hook_portal_session_organization_idx": { + "name": "hook_portal_session_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hook_portal_session_created_by_user_id_user_id_fk": { + "name": "hook_portal_session_created_by_user_id_user_id_fk", + "tableFrom": "hook_portal_session", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "hook_portal_session_application_fk": { + "name": "hook_portal_session_application_fk", + "tableFrom": "hook_portal_session", + "tableTo": "hook_application", + "columnsFrom": [ + "application_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hook_portal_session_token_hash_unique": { + "name": "hook_portal_session_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hook_portal_settings": { + "name": "hook_portal_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_origins": { + "name": "allowed_origins", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "hook_portal_settings_environment_fk": { + "name": "hook_portal_settings_environment_fk", + "tableFrom": "hook_portal_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "hook_portal_settings_environment_unique": { + "name": "hook_portal_settings_environment_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbound_drop_counter": { + "name": "inbound_drop_counter", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "inbound_drop_counter_organization_id_organization_id_fk": { + "name": "inbound_drop_counter_organization_id_organization_id_fk", + "tableFrom": "inbound_drop_counter", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "inbound_drop_counter_pk": { + "name": "inbound_drop_counter_pk", + "columns": [ + "organization_id", + "day" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbound_fake_object": { + "name": "inbound_fake_object", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbound_fake_object_organization_idx": { + "name": "inbound_fake_object_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbound_fake_object_created_at_idx": { + "name": "inbound_fake_object_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_drain": { + "name": "log_drain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sources": { + "name": "sources", + "type": "log_drain_source[]", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'{}'::log_drain_source[]" + }, + "secret_ciphertext": { + "name": "secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "format": { + "name": "format", + "type": "log_drain_format", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ndjson'" + }, + "auth_header_name": { + "name": "auth_header_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_header_ciphertext": { + "name": "auth_header_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "log_drain_environment_idx": { + "name": "log_drain_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"log_drain\".\"enabled\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "log_drain_created_by_user_id_user_id_fk": { + "name": "log_drain_created_by_user_id_user_id_fk", + "tableFrom": "log_drain", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "log_drain_environment_fk": { + "name": "log_drain_environment_fk", + "tableFrom": "log_drain", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "log_drain_id_organization_unique": { + "name": "log_drain_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "log_drain_auth_header_check": { + "name": "log_drain_auth_header_check", + "value": "(\"log_drain\".\"auth_header_name\" is null) = (\"log_drain\".\"auth_header_ciphertext\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.log_drain_attempt": { + "name": "log_drain_attempt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "batch_id": { + "name": "batch_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_excerpt": { + "name": "response_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "log_drain_attempt_batch_idx": { + "name": "log_drain_attempt_batch_idx", + "columns": [ + { + "expression": "batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "log_drain_attempt_batch_fk": { + "name": "log_drain_attempt_batch_fk", + "tableFrom": "log_drain_attempt", + "tableTo": "log_drain_batch", + "columnsFrom": [ + "batch_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_drain_batch": { + "name": "log_drain_batch", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "log_drain_batch_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "record_count": { + "name": "record_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_status_code": { + "name": "last_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "log_drain_batch_pending_unique": { + "name": "log_drain_batch_pending_unique", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"log_drain_batch\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_drain_batch_due_idx": { + "name": "log_drain_batch_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"log_drain_batch\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_drain_batch_drain_created_at_idx": { + "name": "log_drain_batch_drain_created_at_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_drain_batch_updated_at_idx": { + "name": "log_drain_batch_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"log_drain_batch\".\"status\" <> 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "log_drain_batch_drain_fk": { + "name": "log_drain_batch_drain_fk", + "tableFrom": "log_drain_batch", + "tableTo": "log_drain", + "columnsFrom": [ + "drain_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "log_drain_batch_id_organization_unique": { + "name": "log_drain_batch_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_drain_record": { + "name": "log_drain_record", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "drain_id": { + "name": "drain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "batch_id": { + "name": "batch_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "log_drain_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "line": { + "name": "line", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "log_drain_record_queued_idx": { + "name": "log_drain_record_queued_idx", + "columns": [ + { + "expression": "drain_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"log_drain_record\".\"batch_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_drain_record_batch_idx": { + "name": "log_drain_record_batch_idx", + "columns": [ + { + "expression": "batch_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "log_drain_record_drain_fk": { + "name": "log_drain_record_drain_fk", + "tableFrom": "log_drain_record", + "tableTo": "log_drain", + "columnsFrom": [ + "drain_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "log_drain_record_batch_fk": { + "name": "log_drain_record_batch_fk", + "tableFrom": "log_drain_record", + "tableTo": "log_drain_batch", + "columnsFrom": [ + "batch_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "log_drain_record_drain_source_unique": { + "name": "log_drain_record_drain_source_unique", + "nullsNotDistinct": false, + "columns": [ + "drain_id", + "source_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.log_entry": { + "name": "log_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "log_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "route": { + "name": "route", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_ip": { + "name": "client_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recipient": { + "name": "recipient", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "search_text": { + "name": "search_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": { + "log_entry_environment_occurred_at_idx": { + "name": "log_entry_environment_occurred_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_entry_environment_request_idx": { + "name": "log_entry_environment_request_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"log_entry\".\"request_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_entry_environment_resource_idx": { + "name": "log_entry_environment_resource_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"log_entry\".\"resource_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "log_entry_search_text_idx": { + "name": "log_entry_search_text_idx", + "columns": [ + { + "expression": "search_text", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "log_entry_environment_fk": { + "name": "log_entry_environment_fk", + "tableFrom": "log_entry", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "log_entry_pkey": { + "name": "log_entry_pkey", + "columns": [ + "occurred_at", + "id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.marketing_settings": { + "name": "marketing_settings", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mailing_address": { + "name": "mailing_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_from": { + "name": "confirmation_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_subject": { + "name": "confirmation_subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_html": { + "name": "confirmation_html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_text": { + "name": "confirmation_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmed_redirect_url": { + "name": "confirmed_redirect_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "double_opt_in_default": { + "name": "double_opt_in_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "marketing_settings_environment_fk": { + "name": "marketing_settings_environment_fk", + "tableFrom": "marketing_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration": { + "name": "migration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "migration_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "migration_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'inspecting'" + }, + "credentials_ciphertext": { + "name": "credentials_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "credential_hint": { + "name": "credential_hint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "inventory": { + "name": "inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "selection": { + "name": "selection", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cursor": { + "name": "cursor", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "progress": { + "name": "progress", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "migration_environment_created_at_idx": { + "name": "migration_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "migration_due_idx": { + "name": "migration_due_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"migration\".\"status\" in ('inspecting', 'queued', 'running')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "migration_created_by_user_id_user_id_fk": { + "name": "migration_created_by_user_id_user_id_fk", + "tableFrom": "migration", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "migration_environment_fk": { + "name": "migration_environment_fk", + "tableFrom": "migration", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "migration_id_organization_unique": { + "name": "migration_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.migration_item": { + "name": "migration_item", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "migration_id": { + "name": "migration_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "migration_item_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_name": { + "name": "source_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_name": { + "name": "target_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "migration_item_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "needs_review": { + "name": "needs_review", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "warnings": { + "name": "warnings", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "migration_item_migration_fk": { + "name": "migration_item_migration_fk", + "tableFrom": "migration_item", + "tableTo": "migration", + "columnsFrom": [ + "migration_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "migration_item_source_unique": { + "name": "migration_item_source_unique", + "nullsNotDistinct": false, + "columns": [ + "migration_id", + "kind", + "source_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_audit_event": { + "name": "org_audit_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "org_audit_event_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "org_audit_actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_label": { + "name": "actor_label", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_label": { + "name": "target_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "client_ip": { + "name": "client_ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "org_audit_event_organization_idx": { + "name": "org_audit_event_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_audit_event_organization_action_idx": { + "name": "org_audit_event_organization_action_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "action", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_audit_event_organization_id_organization_id_fk": { + "name": "org_audit_event_organization_id_organization_id_fk", + "tableFrom": "org_audit_event", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_api_version": { + "name": "organization_api_version", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_api_version_organization_id_organization_id_fk": { + "name": "organization_api_version_organization_id_organization_id_fk", + "tableFrom": "organization_api_version", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_api_version_updated_by_user_id_user_id_fk": { + "name": "organization_api_version_updated_by_user_id_user_id_fk", + "tableFrom": "organization_api_version", + "tableTo": "user", + "columnsFrom": [ + "updated_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_deletion": { + "name": "organization_deletion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_slug": { + "name": "organization_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_email": { + "name": "requested_by_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "by_platform_admin": { + "name": "by_platform_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "status": { + "name": "status", + "type": "organization_deletion_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_deletion_next_attempt_idx": { + "name": "organization_deletion_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"organization_deletion\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_deletion_org_unique": { + "name": "organization_deletion_org_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_hipaa_mode": { + "name": "organization_hipaa_mode", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "baa_signed_at": { + "name": "baa_signed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "baa_reference": { + "name": "baa_reference", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled_by_user_id": { + "name": "enabled_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_hipaa_mode_organization_id_organization_id_fk": { + "name": "organization_hipaa_mode_organization_id_organization_id_fk", + "tableFrom": "organization_hipaa_mode", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_hipaa_mode_enabled_by_user_id_user_id_fk": { + "name": "organization_hipaa_mode_enabled_by_user_id_user_id_fk", + "tableFrom": "organization_hipaa_mode", + "tableTo": "user", + "columnsFrom": [ + "enabled_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_logo": { + "name": "organization_logo", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "organization_logo_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "failures": { + "name": "failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_logo_organization_id_organization_id_fk": { + "name": "organization_logo_organization_id_organization_id_fk", + "tableFrom": "organization_logo", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_suspension": { + "name": "organization_suspension", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "suspended_by_user_id": { + "name": "suspended_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_suspension_organization_id_organization_id_fk": { + "name": "organization_suspension_organization_id_organization_id_fk", + "tableFrom": "organization_suspension", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_suspension_suspended_by_user_id_user_id_fk": { + "name": "organization_suspension_suspended_by_user_id_user_id_fk", + "tableFrom": "organization_suspension", + "tableTo": "user", + "columnsFrom": [ + "suspended_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.product_preference": { + "name": "product_preference", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product": { + "name": "product", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pinned": { + "name": "pinned", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "product_preference_organization_id_organization_id_fk": { + "name": "product_preference_organization_id_organization_id_fk", + "tableFrom": "product_preference", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "product_preference_user_id_user_id_fk": { + "name": "product_preference_user_id_user_id_fk", + "tableFrom": "product_preference", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "product_preference_member_unique": { + "name": "product_preference_member_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "user_id", + "product" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project": { + "name": "project", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "project_organization_id_organization_id_fk": { + "name": "project_organization_id_organization_id_fk", + "tableFrom": "project", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "project_organization_slug_unique": { + "name": "project_organization_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + }, + "project_id_organization_unique": { + "name": "project_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_override": { + "name": "rate_limit_override", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limit": { + "name": "limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "period_ms": { + "name": "period_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "burst": { + "name": "burst", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "rate_limit_override_organization_id_organization_id_fk": { + "name": "rate_limit_override_organization_id_organization_id_fk", + "tableFrom": "rate_limit_override", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rate_limit_override_organization_bucket_unique": { + "name": "rate_limit_override_organization_bucket_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "bucket" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.received_email": { + "name": "received_email", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "received_email_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "blocked_reason": { + "name": "blocked_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "mail_from": { + "name": "mail_from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "recipients": { + "name": "recipients", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "from": { + "name": "from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "to": { + "name": "to", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "cc": { + "name": "cc", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "reply_to": { + "name": "reply_to", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "in_reply_to": { + "name": "in_reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "references": { + "name": "references", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "html": { + "name": "html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "truncated": { + "name": "truncated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "spam_verdict": { + "name": "spam_verdict", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "virus_verdict": { + "name": "virus_verdict", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "spf_verdict": { + "name": "spf_verdict", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dkim_verdict": { + "name": "dkim_verdict", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dmarc_verdict": { + "name": "dmarc_verdict", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dmarc_policy": { + "name": "dmarc_policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "raw_key": { + "name": "raw_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "received_email_environment_id_idx": { + "name": "received_email_environment_id_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "received_email_created_at_idx": { + "name": "received_email_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "received_email_organization_idx": { + "name": "received_email_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "received_email_environment_fk": { + "name": "received_email_environment_fk", + "tableFrom": "received_email", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "received_email_provider_message_unique": { + "name": "received_email_provider_message_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "provider", + "provider_message_id" + ] + }, + "received_email_id_organization_unique": { + "name": "received_email_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.received_email_attachment": { + "name": "received_email_attachment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "received_email_id": { + "name": "received_email_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disposition": { + "name": "disposition", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_key": { + "name": "content_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "received_email_attachment_email_fk": { + "name": "received_email_attachment_email_fk", + "tableFrom": "received_email_attachment", + "tableTo": "received_email", + "columnsFrom": [ + "received_email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "received_email_attachment_position_unique": { + "name": "received_email_attachment_position_unique", + "nullsNotDistinct": false, + "columns": [ + "received_email_id", + "position" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.receiving_domain": { + "name": "receiving_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "record": { + "name": "record", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "receiving_domain_environment_idx": { + "name": "receiving_domain_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "receiving_domain_next_check_at_idx": { + "name": "receiving_domain_next_check_at_idx", + "columns": [ + { + "expression": "next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"receiving_domain\".\"next_check_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "receiving_domain_domain_fk": { + "name": "receiving_domain_domain_fk", + "tableFrom": "receiving_domain", + "tableTo": "domain", + "columnsFrom": [ + "domain_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "receiving_domain_environment_fk": { + "name": "receiving_domain_environment_fk", + "tableFrom": "receiving_domain", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "receiving_domain_domain_unique": { + "name": "receiving_domain_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain_id" + ] + }, + "receiving_domain_hostname_unique": { + "name": "receiving_domain_hostname_unique", + "nullsNotDistinct": false, + "columns": [ + "hostname" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reputation_event": { + "name": "reputation_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "reputation_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor": { + "name": "actor", + "type": "reputation_actor", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reputation_event_organization_created_at_idx": { + "name": "reputation_event_organization_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reputation_event_organization_id_organization_id_fk": { + "name": "reputation_event_organization_id_organization_id_fk", + "tableFrom": "reputation_event", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reputation_event_actor_user_id_user_id_fk": { + "name": "reputation_event_actor_user_id_user_id_fk", + "tableFrom": "reputation_event", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "reputation_event_id_organization_unique": { + "name": "reputation_event_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reputation_notification": { + "name": "reputation_notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reputation_event_id": { + "name": "reputation_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "reputation_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "reputation_notification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recipients": { + "name": "recipients", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reputation_notification_event_idx": { + "name": "reputation_notification_event_idx", + "columns": [ + { + "expression": "reputation_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reputation_notification_next_attempt_idx": { + "name": "reputation_notification_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reputation_notification\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reputation_notification_organization_id_organization_id_fk": { + "name": "reputation_notification_organization_id_organization_id_fk", + "tableFrom": "reputation_notification", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "reputation_notification_event_fk": { + "name": "reputation_notification_event_fk", + "tableFrom": "reputation_notification", + "tableTo": "reputation_event", + "columnsFrom": [ + "reputation_event_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reputation_standing": { + "name": "reputation_standing", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "reputation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'good'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_changed_at": { + "name": "status_changed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reinstated_at": { + "name": "reinstated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "marketing_paused_at": { + "name": "marketing_paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "marketing_reason": { + "name": "marketing_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "marketing_reinstated_at": { + "name": "marketing_reinstated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "policy_overrides": { + "name": "policy_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metrics": { + "name": "metrics", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_evaluated_at": { + "name": "last_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "evaluation_lease_until": { + "name": "evaluation_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_evaluation_at": { + "name": "next_evaluation_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "reputation_standing_next_evaluation_idx": { + "name": "reputation_standing_next_evaluation_idx", + "columns": [ + { + "expression": "next_evaluation_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reputation_standing\".\"next_evaluation_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reputation_standing_organization_id_organization_id_fk": { + "name": "reputation_standing_organization_id_organization_id_fk", + "tableFrom": "reputation_standing", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_assessment": { + "name": "risk_assessment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "risk_assessment_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "risk_event", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "event_name": { + "name": "event_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "inet", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_hash": { + "name": "email_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip_hash": { + "name": "ip_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subnet_hash": { + "name": "subnet_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "card_hash": { + "name": "card_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "device_hash": { + "name": "device_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "risk_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "score": { + "name": "score", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "reasons": { + "name": "reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "signals": { + "name": "signals", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "degraded": { + "name": "degraded", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "outcome": { + "name": "outcome", + "type": "risk_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "outcome_at": { + "name": "outcome_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "review_status": { + "name": "review_status", + "type": "risk_review_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "personal_data_purged_at": { + "name": "personal_data_purged_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_assessment_environment_sequence_idx": { + "name": "risk_assessment_environment_sequence_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_assessment_environment_user_idx": { + "name": "risk_assessment_environment_user_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"risk_assessment\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_assessment_open_review_idx": { + "name": "risk_assessment_open_review_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"risk_assessment\".\"review_status\" = 'open'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_assessment_created_at_idx": { + "name": "risk_assessment_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_assessment_personal_data_idx": { + "name": "risk_assessment_personal_data_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"risk_assessment\".\"personal_data_purged_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_assessment_environment_fk": { + "name": "risk_assessment_environment_fk", + "tableFrom": "risk_assessment", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "risk_assessment_id_organization_unique": { + "name": "risk_assessment_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "risk_assessment_environment_idempotency_key_unique": { + "name": "risk_assessment_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_assessment_preparation": { + "name": "risk_assessment_preparation", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "risk_assessment_preparation_created_idx": { + "name": "risk_assessment_preparation_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_assessment_preparation_environment_id_environment_id_fk": { + "name": "risk_assessment_preparation_environment_id_environment_id_fk", + "tableFrom": "risk_assessment_preparation", + "tableTo": "environment", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "risk_assessment_preparation_environment_id_idempotency_key_pk": { + "name": "risk_assessment_preparation_environment_id_idempotency_key_pk", + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_dataset": { + "name": "risk_dataset", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entries": { + "name": "entries", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_domain_age": { + "name": "risk_domain_age", + "schema": "", + "columns": { + "domain": { + "name": "domain", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "registered_at": { + "name": "registered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_feedback": { + "name": "risk_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assessment_id": { + "name": "assessment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "risk_outcome", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "risk_feedback_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_feedback_assessment_idx": { + "name": "risk_feedback_assessment_idx", + "columns": [ + { + "expression": "assessment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_feedback_assessment_fk": { + "name": "risk_feedback_assessment_fk", + "tableFrom": "risk_feedback", + "tableTo": "risk_assessment", + "columnsFrom": [ + "assessment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_identity": { + "name": "risk_identity", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "risk_identity_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "value_hash": { + "name": "value_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assessment_id": { + "name": "assessment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_identity_environment_user_idx": { + "name": "risk_identity_environment_user_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"risk_identity\".\"user_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_identity_last_seen_idx": { + "name": "risk_identity_last_seen_idx", + "columns": [ + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_identity_environment_fk": { + "name": "risk_identity_environment_fk", + "tableFrom": "risk_identity", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "risk_identity_value_user_unique": { + "name": "risk_identity_value_user_unique", + "nullsNotDistinct": true, + "columns": [ + "environment_id", + "kind", + "value_hash", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_limit_bucket": { + "name": "risk_limit_bucket", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "limit": { + "name": "limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "period_ms": { + "name": "period_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "burst": { + "name": "burst", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "on_failure": { + "name": "on_failure", + "type": "risk_limit_failure_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'allow'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "risk_limit_bucket_environment_fk": { + "name": "risk_limit_bucket_environment_fk", + "tableFrom": "risk_limit_bucket", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "risk_limit_bucket_name_unique": { + "name": "risk_limit_bucket_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": { + "risk_limit_bucket_values_check": { + "name": "risk_limit_bucket_values_check", + "value": "\"risk_limit_bucket\".\"limit\" > 0 and \"risk_limit_bucket\".\"period_ms\" > 0 and \"risk_limit_bucket\".\"burst\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.risk_list_entry": { + "name": "risk_list_entry", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "risk_list_entry_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "list": { + "name": "list", + "type": "risk_list", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "risk_list_entry_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "network": { + "name": "network", + "type": "cidr", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_list_entry_environment_sequence_idx": { + "name": "risk_list_entry_environment_sequence_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_list_entry_network_idx": { + "name": "risk_list_entry_network_idx", + "columns": [ + { + "expression": "network", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "inet_ops" + } + ], + "isUnique": false, + "where": "\"risk_list_entry\".\"network\" is not null", + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": { + "risk_list_entry_environment_fk": { + "name": "risk_list_entry_environment_fk", + "tableFrom": "risk_list_entry", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "risk_list_entry_value_unique": { + "name": "risk_list_entry_value_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "list", + "type", + "value" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_report": { + "name": "risk_report", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "risk_report_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "value_hash": { + "name": "value_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assessment_id": { + "name": "assessment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reported_at": { + "name": "reported_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_report_assessment_idx": { + "name": "risk_report_assessment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assessment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "risk_report_reported_at_idx": { + "name": "risk_report_reported_at_idx", + "columns": [ + { + "expression": "reported_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_report_environment_fk": { + "name": "risk_report_environment_fk", + "tableFrom": "risk_report", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "risk_report_value_unique": { + "name": "risk_report_value_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "kind", + "value_hash", + "assessment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_rule": { + "name": "risk_rule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "events": { + "name": "events", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "match": { + "name": "match", + "type": "risk_rule_match", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'all'" + }, + "conditions": { + "name": "conditions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "risk_decision", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_rule_environment_position_idx": { + "name": "risk_rule_environment_position_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_rule_environment_fk": { + "name": "risk_rule_environment_fk", + "tableFrom": "risk_rule", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.risk_settings": { + "name": "risk_settings", + "schema": "", + "columns": { + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "review_threshold": { + "name": "review_threshold", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "block_threshold": { + "name": "block_threshold", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "disabled_reasons": { + "name": "disabled_reasons", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "captcha_provider": { + "name": "captcha_provider", + "type": "risk_captcha_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "captcha_secret_ciphertext": { + "name": "captcha_secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "captcha_required": { + "name": "captcha_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content_thresholds": { + "name": "content_thresholds", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "reason_weights": { + "name": "reason_weights", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "risk_settings_environment_fk": { + "name": "risk_settings_environment_fk", + "tableFrom": "risk_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "risk_settings_thresholds_check": { + "name": "risk_settings_thresholds_check", + "value": "\"risk_settings\".\"review_threshold\" > 0 and \"risk_settings\".\"review_threshold\" <= \"risk_settings\".\"block_threshold\" and \"risk_settings\".\"block_threshold\" <= 1" + }, + "risk_settings_captcha_check": { + "name": "risk_settings_captcha_check", + "value": "(\"risk_settings\".\"captcha_provider\" is null) = (\"risk_settings\".\"captcha_secret_ciphertext\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.risk_user_network": { + "name": "risk_user_network", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_hash": { + "name": "user_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "risk_user_network_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "risk_user_network_last_seen_idx": { + "name": "risk_user_network_last_seen_idx", + "columns": [ + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "risk_user_network_environment_fk": { + "name": "risk_user_network_environment_fk", + "tableFrom": "risk_user_network", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "risk_user_network_value_unique": { + "name": "risk_user_network_value_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "user_hash", + "kind", + "value" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scim_group_role": { + "name": "scim_group_role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "group_name": { + "name": "group_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scim_group_role_organization_id_organization_id_fk": { + "name": "scim_group_role_organization_id_organization_id_fk", + "tableFrom": "scim_group_role", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scim_group_role_organization_group_unique": { + "name": "scim_group_role_organization_group_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "group_name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.segment": { + "name": "segment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filter": { + "name": "filter", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "segment_environment_fk": { + "name": "segment_environment_fk", + "tableFrom": "segment", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "segment_environment_name_unique": { + "name": "segment_environment_name_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.topic": { + "name": "topic", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_subscription": { + "name": "default_subscription", + "type": "topic_default_subscription", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "topic_visibility", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "topic_environment_fk": { + "name": "topic_environment_fk", + "tableFrom": "topic", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "topic_environment_key_unique": { + "name": "topic_environment_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "key" + ] + }, + "topic_id_organization_unique": { + "name": "topic_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tracking_domain": { + "name": "tracking_domain", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "tracking_domain_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_hostname_id": { + "name": "provider_hostname_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "record": { + "name": "record", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "provider_message": { + "name": "provider_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verification_started_at": { + "name": "verification_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_checked_at": { + "name": "last_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failed_at": { + "name": "failed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tracking_domain_hostname_active_unique": { + "name": "tracking_domain_hostname_active_unique", + "columns": [ + { + "expression": "hostname", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"tracking_domain\".\"status\" <> 'failed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "tracking_domain_next_check_at_idx": { + "name": "tracking_domain_next_check_at_idx", + "columns": [ + { + "expression": "next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"tracking_domain\".\"next_check_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tracking_domain_domain_fk": { + "name": "tracking_domain_domain_fk", + "tableFrom": "tracking_domain", + "tableTo": "domain", + "columnsFrom": [ + "domain_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tracking_domain_domain_unique": { + "name": "tracking_domain_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "domain_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_attempt": { + "name": "webhook_attempt", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempted_at": { + "name": "attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "response_excerpt": { + "name": "response_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webhook_attempt_delivery_idx": { + "name": "webhook_attempt_delivery_idx", + "columns": [ + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_attempt_delivery_fk": { + "name": "webhook_attempt_delivery_fk", + "tableFrom": "webhook_attempt", + "tableTo": "webhook_delivery", + "columnsFrom": [ + "delivery_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_delivery": { + "name": "webhook_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "endpoint_id": { + "name": "endpoint_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "webhook_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_status_code": { + "name": "last_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_delivery_due_idx": { + "name": "webhook_delivery_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook_delivery\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_delivery_endpoint_created_at_idx": { + "name": "webhook_delivery_endpoint_created_at_idx", + "columns": [ + { + "expression": "endpoint_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_delivery_message_fk": { + "name": "webhook_delivery_message_fk", + "tableFrom": "webhook_delivery", + "tableTo": "webhook_message", + "columnsFrom": [ + "message_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_delivery_endpoint_fk": { + "name": "webhook_delivery_endpoint_fk", + "tableFrom": "webhook_delivery", + "tableTo": "webhook_endpoint", + "columnsFrom": [ + "endpoint_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webhook_delivery_message_endpoint_unique": { + "name": "webhook_delivery_message_endpoint_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id", + "endpoint_id" + ] + }, + "webhook_delivery_id_organization_unique": { + "name": "webhook_delivery_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_endpoint": { + "name": "webhook_endpoint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_version": { + "name": "api_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "secret_ciphertext": { + "name": "secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "previous_secret_ciphertext": { + "name": "previous_secret_ciphertext", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_secret_expires_at": { + "name": "previous_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "disabled_reason": { + "name": "disabled_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failing_since": { + "name": "failing_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_endpoint_environment_idx": { + "name": "webhook_endpoint_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_endpoint_created_by_user_id_user_id_fk": { + "name": "webhook_endpoint_created_by_user_id_user_id_fk", + "tableFrom": "webhook_endpoint", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "webhook_endpoint_environment_fk": { + "name": "webhook_endpoint_environment_fk", + "tableFrom": "webhook_endpoint", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webhook_endpoint_id_organization_unique": { + "name": "webhook_endpoint_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "webhook_endpoint_previous_secret_check": { + "name": "webhook_endpoint_previous_secret_check", + "value": "(\"webhook_endpoint\".\"previous_secret_ciphertext\" is null) = (\"webhook_endpoint\".\"previous_secret_expires_at\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.webhook_listener": { + "name": "webhook_listener", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_types": { + "name": "event_types", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_api_key_id": { + "name": "created_by_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_listener_environment_expires_idx": { + "name": "webhook_listener_environment_expires_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_listener_expires_at_idx": { + "name": "webhook_listener_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_listener_environment_fk": { + "name": "webhook_listener_environment_fk", + "tableFrom": "webhook_listener", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webhook_listener_id_organization_unique": { + "name": "webhook_listener_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_listener_delivery": { + "name": "webhook_listener_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "listener_id": { + "name": "listener_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "webhook_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "handed_out_count": { + "name": "handed_out_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "forwarded_at": { + "name": "forwarded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_listener_delivery_pending_idx": { + "name": "webhook_listener_delivery_pending_idx", + "columns": [ + { + "expression": "listener_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook_listener_delivery\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_listener_delivery_listener_idx": { + "name": "webhook_listener_delivery_listener_idx", + "columns": [ + { + "expression": "listener_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_listener_delivery_listener_fk": { + "name": "webhook_listener_delivery_listener_fk", + "tableFrom": "webhook_listener_delivery", + "tableTo": "webhook_listener", + "columnsFrom": [ + "listener_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_listener_delivery_message_fk": { + "name": "webhook_listener_delivery_message_fk", + "tableFrom": "webhook_listener_delivery", + "tableTo": "webhook_message", + "columnsFrom": [ + "message_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webhook_listener_delivery_message_listener_unique": { + "name": "webhook_listener_delivery_message_listener_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id", + "listener_id" + ] + }, + "webhook_listener_delivery_id_organization_unique": { + "name": "webhook_listener_delivery_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_message": { + "name": "webhook_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_message_environment_idx": { + "name": "webhook_message_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_message_created_at_idx": { + "name": "webhook_message_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_message_environment_fk": { + "name": "webhook_message_environment_fk", + "tableFrom": "webhook_message", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "webhook_message_id_organization_unique": { + "name": "webhook_message_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form": { + "name": "form", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "fields": { + "name": "fields", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "strict_fields": { + "name": "strict_fields", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "allowed_origins": { + "name": "allowed_origins", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "redirect_url": { + "name": "redirect_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "honeypot_field": { + "name": "honeypot_field", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'_gotcha'" + }, + "captcha": { + "name": "captcha", + "type": "form_captcha", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'off'" + }, + "captcha_site_key": { + "name": "captcha_site_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "on_review": { + "name": "on_review", + "type": "form_on_review", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'hold'" + }, + "content_threshold": { + "name": "content_threshold", + "type": "double precision", + "primaryKey": false, + "notNull": false, + "default": 0.9 + }, + "email_field": { + "name": "email_field", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'email'" + }, + "verify_submitter": { + "name": "verify_submitter", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "notify_recipients": { + "name": "notify_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "notification_from": { + "name": "notification_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_from": { + "name": "confirmation_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hosted": { + "name": "hosted", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hosted_published_at": { + "name": "hosted_published_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "disabled_by_admin_at": { + "name": "disabled_by_admin_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_environment_created_at_idx": { + "name": "form_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_environment_fk": { + "name": "form_environment_fk", + "tableFrom": "form", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "form_id_organization_unique": { + "name": "form_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_delivery": { + "name": "form_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "submission_id": { + "name": "submission_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "form_delivery_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "form_delivery_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_delivery_due_idx": { + "name": "form_delivery_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"form_delivery\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_delivery_submission_fk": { + "name": "form_delivery_submission_fk", + "tableFrom": "form_delivery", + "tableTo": "form_submission", + "columnsFrom": [ + "submission_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "form_delivery_submission_kind_unique": { + "name": "form_delivery_submission_kind_unique", + "nullsNotDistinct": false, + "columns": [ + "submission_id", + "kind" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_fake_object": { + "name": "form_fake_object", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_fake_object_organization_idx": { + "name": "form_fake_object_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_store_cleanup": { + "name": "form_store_cleanup", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_store_cleanup_next_attempt_at_idx": { + "name": "form_store_cleanup_next_attempt_at_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_store_cleanup_organization_idx": { + "name": "form_store_cleanup_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_submission": { + "name": "form_submission", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "form_submission_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "submitter_email": { + "name": "submitter_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "referrer": { + "name": "referrer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_assessment_id": { + "name": "risk_assessment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_decision": { + "name": "risk_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_score": { + "name": "risk_score", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "spam_reason": { + "name": "spam_reason", + "type": "form_spam_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "confirmation_token_hash": { + "name": "confirmation_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confirmation_expires_at": { + "name": "confirmation_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_by": { + "name": "released_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_submission_form_created_at_idx": { + "name": "form_submission_form_created_at_idx", + "columns": [ + { + "expression": "form_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_submission_created_at_idx": { + "name": "form_submission_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "form_submission_environment_idx": { + "name": "form_submission_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_submission_form_fk": { + "name": "form_submission_form_fk", + "tableFrom": "form_submission", + "tableTo": "form", + "columnsFrom": [ + "form_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "form_submission_confirmation_token_unique": { + "name": "form_submission_confirmation_token_unique", + "nullsNotDistinct": false, + "columns": [ + "confirmation_token_hash" + ] + }, + "form_submission_id_organization_unique": { + "name": "form_submission_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.form_submission_file": { + "name": "form_submission_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "submission_id": { + "name": "submission_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field": { + "name": "field", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "store_key": { + "name": "store_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "form_submission_file_submission_idx": { + "name": "form_submission_file_submission_idx", + "columns": [ + { + "expression": "submission_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "form_submission_file_submission_fk": { + "name": "form_submission_file_submission_fk", + "tableFrom": "form_submission_file", + "tableTo": "form_submission", + "columnsFrom": [ + "submission_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox": { + "name": "inbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "inbox_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fee_at": { + "name": "next_fee_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_environment_id_idx": { + "name": "inbox_environment_id_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_organization_idx": { + "name": "inbox_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_next_fee_at_idx": { + "name": "inbox_next_fee_at_idx", + "columns": [ + { + "expression": "next_fee_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"inbox\".\"next_fee_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_environment_fk": { + "name": "inbox_environment_fk", + "tableFrom": "inbox", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "inbox_environment_address_unique": { + "name": "inbox_environment_address_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "address" + ] + }, + "inbox_environment_client_id_unique": { + "name": "inbox_environment_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "client_id" + ] + }, + "inbox_id_organization_unique": { + "name": "inbox_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_message": { + "name": "inbox_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inbox_id": { + "name": "inbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thread_id": { + "name": "thread_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "inbox_message_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "received_email_id": { + "name": "received_email_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rfc_message_id": { + "name": "rfc_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_message_thread_idx": { + "name": "inbox_message_thread_idx", + "columns": [ + { + "expression": "thread_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_message_inbox_id_idx": { + "name": "inbox_message_inbox_id_idx", + "columns": [ + { + "expression": "inbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_message_inbox_rfc_message_id_idx": { + "name": "inbox_message_inbox_rfc_message_id_idx", + "columns": [ + { + "expression": "inbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rfc_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"inbox_message\".\"rfc_message_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_message_received_email_idx": { + "name": "inbox_message_received_email_idx", + "columns": [ + { + "expression": "received_email_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_message_organization_idx": { + "name": "inbox_message_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_message_thread_fk": { + "name": "inbox_message_thread_fk", + "tableFrom": "inbox_message", + "tableTo": "inbox_thread", + "columnsFrom": [ + "thread_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inbox_message_received_email_fk": { + "name": "inbox_message_received_email_fk", + "tableFrom": "inbox_message", + "tableTo": "received_email", + "columnsFrom": [ + "received_email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "inbox_message_email_fk": { + "name": "inbox_message_email_fk", + "tableFrom": "inbox_message", + "tableTo": "email", + "columnsFrom": [ + "email_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "inbox_message_inbox_received_email_unique": { + "name": "inbox_message_inbox_received_email_unique", + "nullsNotDistinct": false, + "columns": [ + "inbox_id", + "received_email_id" + ] + }, + "inbox_message_email_unique": { + "name": "inbox_message_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "inbox_message_source_check": { + "name": "inbox_message_source_check", + "value": "(\"inbox_message\".\"direction\" = 'inbound' and \"inbox_message\".\"received_email_id\" is not null and \"inbox_message\".\"email_id\" is null) or (\"inbox_message\".\"direction\" = 'outbound' and \"inbox_message\".\"email_id\" is not null and \"inbox_message\".\"received_email_id\" is null)" + } + }, + "isRLSEnabled": false + }, + "public.inbox_thread": { + "name": "inbox_thread", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inbox_id": { + "name": "inbox_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject_key": { + "name": "subject_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "participants": { + "name": "participants", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_thread_inbox_last_message_idx": { + "name": "inbox_thread_inbox_last_message_idx", + "columns": [ + { + "expression": "inbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_message_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_thread_inbox_subject_idx": { + "name": "inbox_thread_inbox_subject_idx", + "columns": [ + { + "expression": "inbox_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_thread_last_message_at_idx": { + "name": "inbox_thread_last_message_at_idx", + "columns": [ + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_thread_organization_idx": { + "name": "inbox_thread_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_thread_inbox_fk": { + "name": "inbox_thread_inbox_fk", + "tableFrom": "inbox_thread", + "tableTo": "inbox", + "columnsFrom": [ + "inbox_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "inbox_thread_id_organization_unique": { + "name": "inbox_thread_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notification": { + "name": "notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "notification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "recipients": { + "name": "recipients", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "notification_dedupe_unique": { + "name": "notification_dedupe_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dedupe_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_next_attempt_idx": { + "name": "notification_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"notification\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "notification_created_at_idx": { + "name": "notification_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "notification_organization_id_organization_id_fk": { + "name": "notification_organization_id_organization_id_fk", + "tableFrom": "notification", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_device": { + "name": "push_device", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apns_environment": { + "name": "apns_environment", + "type": "apns_environment", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "categories": { + "name": "categories", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "push_device_session_idx": { + "name": "push_device_session_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_device_token_idx": { + "name": "push_device_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_device_user_idx": { + "name": "push_device_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_device_user_id_user_id_fk": { + "name": "push_device_user_id_user_id_fk", + "tableFrom": "push_device", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "push_device_session_id_session_id_fk": { + "name": "push_device_session_id_session_id_fk", + "tableFrom": "push_device", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.push_notification": { + "name": "push_notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "push_notification_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "collapse_key": { + "name": "collapse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "count": { + "name": "count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "status": { + "name": "status", + "type": "push_notification_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_device_ids": { + "name": "delivered_device_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "push_notification_pending_collapse_idx": { + "name": "push_notification_pending_collapse_idx", + "columns": [ + { + "expression": "collapse_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"push_notification\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_notification_collapse_sent_idx": { + "name": "push_notification_collapse_sent_idx", + "columns": [ + { + "expression": "collapse_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sent_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"push_notification\".\"collapse_key\" IS NOT NULL AND \"push_notification\".\"sent_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_notification_next_attempt_idx": { + "name": "push_notification_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"push_notification\".\"next_attempt_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_notification_created_at_idx": { + "name": "push_notification_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "push_notification_organization_idx": { + "name": "push_notification_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "push_notification_organization_id_organization_id_fk": { + "name": "push_notification_organization_id_organization_id_fk", + "tableFrom": "push_notification", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "push_notification_environment_fk": { + "name": "push_notification_environment_fk", + "tableFrom": "push_notification", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_attendee": { + "name": "scheduling_attendee", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "scheduling_attendee_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'attendee'" + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_contact": { + "name": "encrypted_contact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_hmac": { + "name": "email_hmac", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_answers": { + "name": "encrypted_answers", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time_zone": { + "name": "time_zone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "locale": { + "name": "locale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redacted_at": { + "name": "redacted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_attendee_booking_idx": { + "name": "scheduling_attendee_booking_idx", + "columns": [ + { + "expression": "booking_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_attendee_environment_uid_idx": { + "name": "scheduling_attendee_environment_uid_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_attendee_environment_email_idx": { + "name": "scheduling_attendee_environment_email_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_attendee_booking_fk": { + "name": "scheduling_attendee_booking_fk", + "tableFrom": "scheduling_attendee", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_booking": { + "name": "scheduling_booking", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "scheduling_booking_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "buffer_before_minutes": { + "name": "buffer_before_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "buffer_after_minutes": { + "name": "buffer_after_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "hold_expires_at": { + "name": "hold_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approval_expires_at": { + "name": "approval_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "series_id": { + "name": "series_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "series_index": { + "name": "series_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "group_session_id": { + "name": "group_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "calendar_unverified": { + "name": "calendar_unverified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_conflict_at": { + "name": "external_conflict_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "no_show_at": { + "name": "no_show_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "redacted_at": { + "name": "redacted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_booking_environment_start_idx": { + "name": "scheduling_booking_environment_start_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_event_type_start_idx": { + "name": "scheduling_booking_event_type_start_idx", + "columns": [ + { + "expression": "event_type_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_series_idx": { + "name": "scheduling_booking_series_idx", + "columns": [ + { + "expression": "series_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "series_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_group_session_idx": { + "name": "scheduling_booking_group_session_idx", + "columns": [ + { + "expression": "group_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_end_idx": { + "name": "scheduling_booking_end_idx", + "columns": [ + { + "expression": "end_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_unredacted_end_idx": { + "name": "scheduling_booking_unredacted_end_idx", + "columns": [ + { + "expression": "end_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_booking\".\"redacted_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_hold_expiry_idx": { + "name": "scheduling_booking_hold_expiry_idx", + "columns": [ + { + "expression": "hold_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_booking\".\"status\" = 'held'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_booking_approval_expiry_idx": { + "name": "scheduling_booking_approval_expiry_idx", + "columns": [ + { + "expression": "approval_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_booking\".\"status\" = 'pending_approval'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_booking_environment_fk": { + "name": "scheduling_booking_environment_fk", + "tableFrom": "scheduling_booking", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_booking_event_type_fk": { + "name": "scheduling_booking_event_type_fk", + "tableFrom": "scheduling_booking", + "tableTo": "scheduling_event_type", + "columnsFrom": [ + "event_type_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_booking_series_fk": { + "name": "scheduling_booking_series_fk", + "tableFrom": "scheduling_booking", + "tableTo": "scheduling_series", + "columnsFrom": [ + "series_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "scheduling_booking_group_session_fk": { + "name": "scheduling_booking_group_session_fk", + "tableFrom": "scheduling_booking", + "tableTo": "scheduling_group_session", + "columnsFrom": [ + "group_session_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_booking_id_organization_unique": { + "name": "scheduling_booking_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_booking_environment_idempotency_unique": { + "name": "scheduling_booking_environment_idempotency_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scheduling_booking_time_order": { + "name": "scheduling_booking_time_order", + "value": "\"scheduling_booking\".\"start_at\" < \"scheduling_booking\".\"end_at\"" + }, + "scheduling_booking_hold_has_expiry": { + "name": "scheduling_booking_hold_has_expiry", + "value": "\"scheduling_booking\".\"status\" <> 'held' OR \"scheduling_booking\".\"hold_expires_at\" IS NOT NULL" + }, + "scheduling_booking_approval_has_expiry": { + "name": "scheduling_booking_approval_has_expiry", + "value": "\"scheduling_booking\".\"status\" <> 'pending_approval' OR \"scheduling_booking\".\"approval_expires_at\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_booking_event": { + "name": "scheduling_booking_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor": { + "name": "actor", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scheduling_booking_event_booking_fk": { + "name": "scheduling_booking_event_booking_fk", + "tableFrom": "scheduling_booking_event", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_booking_event_sequence_unique": { + "name": "scheduling_booking_event_sequence_unique", + "nullsNotDistinct": false, + "columns": [ + "booking_id", + "sequence" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_booking_host": { + "name": "scheduling_booking_host", + "schema": "", + "columns": { + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "scheduling_host_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + } + }, + "indexes": { + "scheduling_booking_host_host_idx": { + "name": "scheduling_booking_host_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_booking_host_booking_fk": { + "name": "scheduling_booking_host_booking_fk", + "tableFrom": "scheduling_booking_host", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_booking_host_host_fk": { + "name": "scheduling_booking_host_host_fk", + "tableFrom": "scheduling_booking_host", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scheduling_booking_host_booking_id_host_id_pk": { + "name": "scheduling_booking_host_booking_id_host_id_pk", + "columns": [ + "booking_id", + "host_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_busy_interval": { + "name": "scheduling_busy_interval", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "calendar_id": { + "name": "calendar_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scheduling_busy_interval_host_idx": { + "name": "scheduling_busy_interval_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "end_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "start_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_busy_interval_calendar_idx": { + "name": "scheduling_busy_interval_calendar_idx", + "columns": [ + { + "expression": "calendar_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_busy_interval_calendar_fk": { + "name": "scheduling_busy_interval_calendar_fk", + "tableFrom": "scheduling_busy_interval", + "tableTo": "scheduling_calendar", + "columnsFrom": [ + "calendar_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_calendar": { + "name": "scheduling_calendar", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_busy": { + "name": "read_busy", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "write_target": { + "name": "write_target", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "covered_until": { + "name": "covered_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dirty_at": { + "name": "dirty_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fetch_started_at": { + "name": "last_fetch_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_sync_at": { + "name": "next_sync_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sync_failures": { + "name": "sync_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "channel_id": { + "name": "channel_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel_resource_id": { + "name": "channel_resource_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel_token_hash": { + "name": "channel_token_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel_expires_at": { + "name": "channel_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_calendar_host_write_target_unique": { + "name": "scheduling_calendar_host_write_target_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"scheduling_calendar\".\"write_target\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_calendar_host_idx": { + "name": "scheduling_calendar_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_calendar_channel_idx": { + "name": "scheduling_calendar_channel_idx", + "columns": [ + { + "expression": "channel_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_calendar_next_sync_idx": { + "name": "scheduling_calendar_next_sync_idx", + "columns": [ + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_calendar_connection_fk": { + "name": "scheduling_calendar_connection_fk", + "tableFrom": "scheduling_calendar", + "tableTo": "scheduling_calendar_connection", + "columnsFrom": [ + "connection_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_calendar_id_organization_unique": { + "name": "scheduling_calendar_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_calendar_connection_external_unique": { + "name": "scheduling_calendar_connection_external_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_id", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_calendar_connection": { + "name": "scheduling_calendar_connection", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "scheduling_calendar_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_credentials": { + "name": "encrypted_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "scheduling_connection_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_at": { + "name": "last_error_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scheduling_calendar_connection_host_fk": { + "name": "scheduling_calendar_connection_host_fk", + "tableFrom": "scheduling_calendar_connection", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_calendar_connection_id_organization_unique": { + "name": "scheduling_calendar_connection_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_calendar_connection_account_unique": { + "name": "scheduling_calendar_connection_account_unique", + "nullsNotDistinct": false, + "columns": [ + "host_id", + "provider", + "account_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_calendar_event": { + "name": "scheduling_calendar_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_session_id": { + "name": "group_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "calendar_id": { + "name": "calendar_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "desired_start": { + "name": "desired_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "desired_end": { + "name": "desired_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "desired_state": { + "name": "desired_state", + "type": "scheduling_calendar_event_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'present'" + }, + "desired_version": { + "name": "desired_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "applied_version": { + "name": "applied_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "provider_event_id": { + "name": "provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_state": { + "name": "external_state", + "type": "scheduling_external_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'in_sync'" + }, + "details_written": { + "name": "details_written", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "scrub_pending": { + "name": "scrub_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_calendar_event_host_idx": { + "name": "scheduling_calendar_event_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "desired_end", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "desired_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_calendar_event_due_idx": { + "name": "scheduling_calendar_event_due_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_calendar_event\".\"applied_version\" < \"scheduling_calendar_event\".\"desired_version\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_calendar_event_booking_fk": { + "name": "scheduling_calendar_event_booking_fk", + "tableFrom": "scheduling_calendar_event", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_calendar_event_group_session_fk": { + "name": "scheduling_calendar_event_group_session_fk", + "tableFrom": "scheduling_calendar_event", + "tableTo": "scheduling_group_session", + "columnsFrom": [ + "group_session_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_calendar_event_calendar_fk": { + "name": "scheduling_calendar_event_calendar_fk", + "tableFrom": "scheduling_calendar_event", + "tableTo": "scheduling_calendar", + "columnsFrom": [ + "calendar_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_calendar_event_target_unique": { + "name": "scheduling_calendar_event_target_unique", + "nullsNotDistinct": false, + "columns": [ + "booking_id", + "host_id", + "calendar_id" + ] + }, + "scheduling_calendar_event_session_target_unique": { + "name": "scheduling_calendar_event_session_target_unique", + "nullsNotDistinct": false, + "columns": [ + "group_session_id", + "host_id", + "calendar_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scheduling_calendar_event_one_owner": { + "name": "scheduling_calendar_event_one_owner", + "value": "(\"scheduling_calendar_event\".\"booking_id\" IS NULL) <> (\"scheduling_calendar_event\".\"group_session_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_claim": { + "name": "scheduling_claim", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "group_session_id": { + "name": "group_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occupied_start": { + "name": "occupied_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "occupied_end": { + "name": "occupied_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "live_from": { + "name": "live_from", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "live_until": { + "name": "live_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "limit_day": { + "name": "limit_day", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "limit_week": { + "name": "limit_week", + "type": "date", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "scheduling_claim_booking_idx": { + "name": "scheduling_claim_booking_idx", + "columns": [ + { + "expression": "booking_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_claim_group_session_idx": { + "name": "scheduling_claim_group_session_idx", + "columns": [ + { + "expression": "group_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_claim_host_day_idx": { + "name": "scheduling_claim_host_day_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "limit_day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_claim_host_week_idx": { + "name": "scheduling_claim_host_week_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "limit_week", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_claim_occupied_end_idx": { + "name": "scheduling_claim_occupied_end_idx", + "columns": [ + { + "expression": "occupied_end", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_claim_live_until_idx": { + "name": "scheduling_claim_live_until_idx", + "columns": [ + { + "expression": "live_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_claim\".\"live_until\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_claim_host_fk": { + "name": "scheduling_claim_host_fk", + "tableFrom": "scheduling_claim", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_claim_booking_fk": { + "name": "scheduling_claim_booking_fk", + "tableFrom": "scheduling_claim", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_claim_group_session_fk": { + "name": "scheduling_claim_group_session_fk", + "tableFrom": "scheduling_claim", + "tableTo": "scheduling_group_session", + "columnsFrom": [ + "group_session_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scheduling_claim_occupied_order": { + "name": "scheduling_claim_occupied_order", + "value": "\"scheduling_claim\".\"occupied_start\" < \"scheduling_claim\".\"occupied_end\"" + }, + "scheduling_claim_live_order": { + "name": "scheduling_claim_live_order", + "value": "\"scheduling_claim\".\"live_until\" IS NULL OR \"scheduling_claim\".\"live_from\" < \"scheduling_claim\".\"live_until\"" + }, + "scheduling_claim_one_owner": { + "name": "scheduling_claim_one_owner", + "value": "(\"scheduling_claim\".\"booking_id\" IS NULL) <> (\"scheduling_claim\".\"group_session_id\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_event_type": { + "name": "scheduling_event_type", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "scheduling_event_type_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'one_on_one'" + }, + "status": { + "name": "status", + "type": "scheduling_event_type_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "duration_minutes": { + "name": "duration_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "duration_options": { + "name": "duration_options", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "step_minutes": { + "name": "step_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "grid_time_zone": { + "name": "grid_time_zone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "buffer_before_minutes": { + "name": "buffer_before_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "buffer_after_minutes": { + "name": "buffer_after_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "min_notice_minutes": { + "name": "min_notice_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "booking_window": { + "name": "booking_window", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "limits": { + "name": "limits", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "requires_approval": { + "name": "requires_approval", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "approval_ttl_minutes": { + "name": "approval_ttl_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1440 + }, + "hold_ttl_seconds": { + "name": "hold_ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 600 + }, + "cancel_notice_minutes": { + "name": "cancel_notice_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reschedule_notice_minutes": { + "name": "reschedule_notice_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_guests": { + "name": "max_guests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "host_calendar_details": { + "name": "host_calendar_details", + "type": "scheduling_calendar_details", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'neutral'" + }, + "questions": { + "name": "questions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "locations": { + "name": "locations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "host_filter": { + "name": "host_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notifications": { + "name": "notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "screened_at": { + "name": "screened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_event_type_environment_created_at_idx": { + "name": "scheduling_event_type_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_event_type_environment_fk": { + "name": "scheduling_event_type_environment_fk", + "tableFrom": "scheduling_event_type", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_event_type_environment_uid_unique": { + "name": "scheduling_event_type_environment_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "uid" + ] + }, + "scheduling_event_type_id_organization_unique": { + "name": "scheduling_event_type_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scheduling_event_type_duration_positive": { + "name": "scheduling_event_type_duration_positive", + "value": "\"scheduling_event_type\".\"duration_minutes\" > 0" + }, + "scheduling_event_type_step_positive": { + "name": "scheduling_event_type_step_positive", + "value": "\"scheduling_event_type\".\"step_minutes\" > 0" + }, + "scheduling_event_type_capacity": { + "name": "scheduling_event_type_capacity", + "value": "\"scheduling_event_type\".\"capacity\" IS NULL OR (\"scheduling_event_type\".\"kind\" = 'group' AND \"scheduling_event_type\".\"capacity\" > 0)" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_event_type_host": { + "name": "scheduling_event_type_host", + "schema": "", + "columns": { + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "scheduling_host_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "weight": { + "name": "weight", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 100 + }, + "assigned": { + "name": "assigned", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "fair_units": { + "name": "fair_units", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_assigned_at": { + "name": "last_assigned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_event_type_host_host_idx": { + "name": "scheduling_event_type_host_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_event_type_host_event_type_fk": { + "name": "scheduling_event_type_host_event_type_fk", + "tableFrom": "scheduling_event_type_host", + "tableTo": "scheduling_event_type", + "columnsFrom": [ + "event_type_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_event_type_host_host_fk": { + "name": "scheduling_event_type_host_host_fk", + "tableFrom": "scheduling_event_type_host", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scheduling_event_type_host_event_type_id_host_id_pk": { + "name": "scheduling_event_type_host_event_type_id_host_id_pk", + "columns": [ + "event_type_id", + "host_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "scheduling_event_type_host_weight_positive": { + "name": "scheduling_event_type_host_weight_positive", + "value": "\"scheduling_event_type_host\".\"weight\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_group_session": { + "name": "scheduling_group_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "start_at": { + "name": "start_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "end_at": { + "name": "end_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "location": { + "name": "location", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "meeting_url": { + "name": "meeting_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_group_session_end_at_idx": { + "name": "scheduling_group_session_end_at_idx", + "columns": [ + { + "expression": "end_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_group_session_event_type_fk": { + "name": "scheduling_group_session_event_type_fk", + "tableFrom": "scheduling_group_session", + "tableTo": "scheduling_event_type", + "columnsFrom": [ + "event_type_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_group_session_host_fk": { + "name": "scheduling_group_session_host_fk", + "tableFrom": "scheduling_group_session", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_group_session_id_organization_unique": { + "name": "scheduling_group_session_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_group_session_slot_unique": { + "name": "scheduling_group_session_slot_unique", + "nullsNotDistinct": false, + "columns": [ + "event_type_id", + "host_id", + "start_at" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_host": { + "name": "scheduling_host", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uid": { + "name": "uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "time_zone": { + "name": "time_zone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attributes": { + "name": "attributes", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "locations": { + "name": "locations", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "status": { + "name": "status", + "type": "scheduling_host_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "screened_at": { + "name": "screened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_fee_at": { + "name": "next_fee_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_host_environment_created_at_idx": { + "name": "scheduling_host_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_host_attributes_idx": { + "name": "scheduling_host_attributes_idx", + "columns": [ + { + "expression": "attributes", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "jsonb_path_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "scheduling_host_next_fee_at_idx": { + "name": "scheduling_host_next_fee_at_idx", + "columns": [ + { + "expression": "next_fee_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_host\".\"next_fee_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_host_live_organization_idx": { + "name": "scheduling_host_live_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_host\".\"next_fee_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_host_environment_fk": { + "name": "scheduling_host_environment_fk", + "tableFrom": "scheduling_host", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_host_environment_uid_unique": { + "name": "scheduling_host_environment_uid_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "uid" + ] + }, + "scheduling_host_id_organization_unique": { + "name": "scheduling_host_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_limit_bucket": { + "name": "scheduling_limit_bucket", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_limit_bucket_organization_idx": { + "name": "scheduling_limit_bucket_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_limit_bucket_created_at_idx": { + "name": "scheduling_limit_bucket_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "scheduling_limit_bucket_scope_scope_id_period_pk": { + "name": "scheduling_limit_bucket_scope_scope_id_period_pk", + "columns": [ + "scope", + "scope_id", + "period" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_logo": { + "name": "scheduling_logo", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scheduling_logo_environment_fk": { + "name": "scheduling_logo_environment_fk", + "tableFrom": "scheduling_logo", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_logo_environment_unique": { + "name": "scheduling_logo_environment_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_notification": { + "name": "scheduling_notification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "scheduling_notification_channel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "recipient": { + "name": "recipient", + "type": "scheduling_notification_recipient", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "booking_version": { + "name": "booking_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "due_at": { + "name": "due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "lease_until": { + "name": "lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_notification_due_idx": { + "name": "scheduling_notification_due_idx", + "columns": [ + { + "expression": "due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_notification\".\"finished_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_notification_booking_idx": { + "name": "scheduling_notification_booking_idx", + "columns": [ + { + "expression": "booking_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_notification_environment_fk": { + "name": "scheduling_notification_environment_fk", + "tableFrom": "scheduling_notification", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_notification_booking_fk": { + "name": "scheduling_notification_booking_fk", + "tableFrom": "scheduling_notification", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_notification_dedupe_unique": { + "name": "scheduling_notification_dedupe_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_oauth_state": { + "name": "scheduling_oauth_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "scheduling_calendar_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_code_verifier": { + "name": "encrypted_code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_oauth_state_expires_at_idx": { + "name": "scheduling_oauth_state_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_oauth_state_host_fk": { + "name": "scheduling_oauth_state_host_fk", + "tableFrom": "scheduling_oauth_state", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_oauth_state_hash_unique": { + "name": "scheduling_oauth_state_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "state_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_schedule": { + "name": "scheduling_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "time_zone": { + "name": "time_zone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rules": { + "name": "rules", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_schedule_host_default_unique": { + "name": "scheduling_schedule_host_default_unique", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"scheduling_schedule\".\"is_default\"", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_schedule_host_idx": { + "name": "scheduling_schedule_host_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_schedule_host_fk": { + "name": "scheduling_schedule_host_fk", + "tableFrom": "scheduling_schedule", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_schedule_id_organization_unique": { + "name": "scheduling_schedule_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_schedule_override": { + "name": "scheduling_schedule_override", + "schema": "", + "columns": { + "schedule_id": { + "name": "schedule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "date": { + "name": "date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "ranges": { + "name": "ranges", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + } + }, + "indexes": {}, + "foreignKeys": { + "scheduling_schedule_override_schedule_fk": { + "name": "scheduling_schedule_override_schedule_fk", + "tableFrom": "scheduling_schedule_override", + "tableTo": "scheduling_schedule", + "columnsFrom": [ + "schedule_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "scheduling_schedule_override_schedule_id_date_pk": { + "name": "scheduling_schedule_override_schedule_id_date_pk", + "columns": [ + "schedule_id", + "date" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_series": { + "name": "scheduling_series", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rule": { + "name": "rule", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "anchor_time_zone": { + "name": "anchor_time_zone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_series_environment_idx": { + "name": "scheduling_series_environment_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_series_created_at_idx": { + "name": "scheduling_series_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_series_event_type_fk": { + "name": "scheduling_series_event_type_fk", + "tableFrom": "scheduling_series", + "tableTo": "scheduling_event_type", + "columnsFrom": [ + "event_type_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_series_id_organization_unique": { + "name": "scheduling_series_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "scheduling_series_environment_idempotency_unique": { + "name": "scheduling_series_environment_idempotency_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.scheduling_session": { + "name": "scheduling_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "scheduling_session_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type_id": { + "name": "event_type_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "booking_id": { + "name": "booking_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_attendee": { + "name": "encrypted_attendee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_attributes": { + "name": "encrypted_attributes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attendee_uid": { + "name": "attendee_uid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attendee_email_hmac": { + "name": "attendee_email_hmac", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "read_only": { + "name": "read_only", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "scheduling_session_expires_at_idx": { + "name": "scheduling_session_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_session_booking_idx": { + "name": "scheduling_session_booking_idx", + "columns": [ + { + "expression": "booking_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_session_attendee_email_idx": { + "name": "scheduling_session_attendee_email_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attendee_email_hmac", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "attendee_email_hmac is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "scheduling_session_attendee_uid_idx": { + "name": "scheduling_session_attendee_uid_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attendee_uid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"scheduling_session\".\"attendee_uid\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "scheduling_session_environment_fk": { + "name": "scheduling_session_environment_fk", + "tableFrom": "scheduling_session", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_session_host_fk": { + "name": "scheduling_session_host_fk", + "tableFrom": "scheduling_session", + "tableTo": "scheduling_host", + "columnsFrom": [ + "host_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_session_event_type_fk": { + "name": "scheduling_session_event_type_fk", + "tableFrom": "scheduling_session", + "tableTo": "scheduling_event_type", + "columnsFrom": [ + "event_type_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "scheduling_session_booking_fk": { + "name": "scheduling_session_booking_fk", + "tableFrom": "scheduling_session", + "tableTo": "scheduling_booking", + "columnsFrom": [ + "booking_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_session_token_hash_unique": { + "name": "scheduling_session_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "scheduling_session_scope": { + "name": "scheduling_session_scope", + "value": "(\"scheduling_session\".\"kind\" = 'host' AND \"scheduling_session\".\"host_id\" IS NOT NULL)\n OR (\"scheduling_session\".\"kind\" = 'booker' AND (\"scheduling_session\".\"event_type_id\" IS NOT NULL OR \"scheduling_session\".\"host_id\" IS NOT NULL))\n OR (\"scheduling_session\".\"kind\" = 'manage' AND \"scheduling_session\".\"booking_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.scheduling_settings": { + "name": "scheduling_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_origins": { + "name": "allowed_origins", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "brand_name": { + "name": "brand_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_from": { + "name": "email_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sms_from": { + "name": "sms_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notifications": { + "name": "notifications", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "calendar_failure_mode": { + "name": "calendar_failure_mode", + "type": "scheduling_calendar_failure_mode", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'hide'" + }, + "screened_at": { + "name": "screened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "scheduling_settings_environment_fk": { + "name": "scheduling_settings_environment_fk", + "tableFrom": "scheduling_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "scheduling_settings_environment_unique": { + "name": "scheduling_settings_environment_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_builder_session": { + "name": "sign_builder_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_builder_session_expires_at_idx": { + "name": "sign_builder_session_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_builder_session_template_fk": { + "name": "sign_builder_session_template_fk", + "tableFrom": "sign_builder_session", + "tableTo": "sign_template", + "columnsFrom": [ + "template_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_builder_session_token_hash_unique": { + "name": "sign_builder_session_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_document": { + "name": "sign_document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "store_key": { + "name": "store_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pages": { + "name": "pages", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "anchors": { + "name": "anchors", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_document_environment_created_at_idx": { + "name": "sign_document_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_document_created_at_idx": { + "name": "sign_document_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_document_environment_fk": { + "name": "sign_document_environment_fk", + "tableFrom": "sign_document", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_document_id_organization_unique": { + "name": "sign_document_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_event": { + "name": "sign_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sign_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "signer_id": { + "name": "signer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor": { + "name": "actor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "previous_hash": { + "name": "previous_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "sign_event_request_fk": { + "name": "sign_event_request_fk", + "tableFrom": "sign_event", + "tableTo": "sign_request", + "columnsFrom": [ + "request_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_event_request_sequence_unique": { + "name": "sign_event_request_sequence_unique", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "sequence" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_fake_object": { + "name": "sign_fake_object", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_fake_object_organization_idx": { + "name": "sign_fake_object_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_field": { + "name": "sign_field", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signer_id": { + "name": "signer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sign_field_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "page": { + "name": "page", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "x": { + "name": "x", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "y": { + "name": "y", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "anchor": { + "name": "anchor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "prefilled": { + "name": "prefilled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_field_request_idx": { + "name": "sign_field_request_idx", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_field_signer_idx": { + "name": "sign_field_signer_idx", + "columns": [ + { + "expression": "signer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_field_signer_fk": { + "name": "sign_field_signer_fk", + "tableFrom": "sign_field", + "tableTo": "sign_signer", + "columnsFrom": [ + "signer_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sign_field_request_fk": { + "name": "sign_field_request_fk", + "tableFrom": "sign_field", + "tableTo": "sign_request", + "columnsFrom": [ + "request_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_request": { + "name": "sign_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sign_request_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signing_order": { + "name": "signing_order", + "type": "sign_signing_order", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'parallel'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "declined_at": { + "name": "declined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "signed_document_key": { + "name": "signed_document_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signed_document_sha256": { + "name": "signed_document_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sealed_at": { + "name": "sealed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "seal_lease_until": { + "name": "seal_lease_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "seal_attempts": { + "name": "seal_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "seal_next_attempt_at": { + "name": "seal_next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "seal_error": { + "name": "seal_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reminder_interval_days": { + "name": "reminder_interval_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "expired_at": { + "name": "expired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_request_environment_created_at_idx": { + "name": "sign_request_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_request_expires_at_idx": { + "name": "sign_request_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sign_request\".\"status\" = 'sent'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_request_created_at_idx": { + "name": "sign_request_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_request_unsealed_idx": { + "name": "sign_request_unsealed_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sign_request\".\"status\" = 'completed' and \"sign_request\".\"sealed_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_request_environment_fk": { + "name": "sign_request_environment_fk", + "tableFrom": "sign_request", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_request_environment_idempotency_key_unique": { + "name": "sign_request_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + }, + "sign_request_id_organization_unique": { + "name": "sign_request_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_request_document": { + "name": "sign_request_document", + "schema": "", + "columns": { + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sign_request_document_document_idx": { + "name": "sign_request_document_document_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_request_document_request_fk": { + "name": "sign_request_document_request_fk", + "tableFrom": "sign_request_document", + "tableTo": "sign_request", + "columnsFrom": [ + "request_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sign_request_document_document_fk": { + "name": "sign_request_document_document_fk", + "tableFrom": "sign_request_document", + "tableTo": "sign_document", + "columnsFrom": [ + "document_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sign_request_document_request_id_position_pk": { + "name": "sign_request_document_request_id_position_pk", + "columns": [ + "request_id", + "position" + ] + } + }, + "uniqueConstraints": { + "sign_request_document_unique": { + "name": "sign_request_document_unique", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "document_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_session": { + "name": "sign_session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "signer_id": { + "name": "signer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_session_expires_at_idx": { + "name": "sign_session_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_session_signer_idx": { + "name": "sign_session_signer_idx", + "columns": [ + { + "expression": "signer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_session_signer_fk": { + "name": "sign_session_signer_fk", + "tableFrom": "sign_session", + "tableTo": "sign_signer", + "columnsFrom": [ + "signer_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_session_token_hash_unique": { + "name": "sign_session_token_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "token_hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_settings": { + "name": "sign_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_origins": { + "name": "allowed_origins", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accent_color": { + "name": "accent_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "disclosure": { + "name": "disclosure", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_from": { + "name": "email_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reminder_interval_days": { + "name": "reminder_interval_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sign_settings_environment_fk": { + "name": "sign_settings_environment_fk", + "tableFrom": "sign_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_settings_environment_unique": { + "name": "sign_settings_environment_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_signer": { + "name": "sign_signer", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "delivery": { + "name": "delivery", + "type": "sign_delivery", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "authentication": { + "name": "authentication", + "type": "sign_authentication", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sign_signer_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "client_ref": { + "name": "client_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "viewed_at": { + "name": "viewed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consented_at": { + "name": "consented_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "signed_at": { + "name": "signed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "declined_at": { + "name": "declined_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "decline_reason": { + "name": "decline_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature_key": { + "name": "signature_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signature_sha256": { + "name": "signature_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initials_key": { + "name": "initials_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "initials_sha256": { + "name": "initials_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notified_at": { + "name": "notified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reminder_count": { + "name": "reminder_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_reminded_at": { + "name": "last_reminded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_reminder_at": { + "name": "next_reminder_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "remind_requested_at": { + "name": "remind_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "copy_sent_at": { + "name": "copy_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "email_failures": { + "name": "email_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "email_retry_at": { + "name": "email_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_signer_request_idx": { + "name": "sign_signer_request_idx", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "position", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_signer_email_due_idx": { + "name": "sign_signer_email_due_idx", + "columns": [ + { + "expression": "request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sign_signer\".\"delivery\" = 'email' and (\"sign_signer\".\"notified_at\" is null or \"sign_signer\".\"next_reminder_at\" is not null or \"sign_signer\".\"remind_requested_at\" is not null or (\"sign_signer\".\"status\" = 'signed' and \"sign_signer\".\"copy_sent_at\" is null))", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_signer_request_fk": { + "name": "sign_signer_request_fk", + "tableFrom": "sign_signer", + "tableTo": "sign_request", + "columnsFrom": [ + "request_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_signer_id_organization_unique": { + "name": "sign_signer_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "sign_signer_request_email_unique": { + "name": "sign_signer_request_email_unique", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_store_cleanup": { + "name": "sign_store_cleanup", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_store_cleanup_next_attempt_at_idx": { + "name": "sign_store_cleanup_next_attempt_at_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sign_store_cleanup_organization_idx": { + "name": "sign_store_cleanup_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_template": { + "name": "sign_template", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signing_order": { + "name": "signing_order", + "type": "sign_signing_order", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'parallel'" + }, + "roles": { + "name": "roles", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_template_environment_created_at_idx": { + "name": "sign_template_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_template_environment_fk": { + "name": "sign_template_environment_fk", + "tableFrom": "sign_template", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sign_template_id_organization_unique": { + "name": "sign_template_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_template_document": { + "name": "sign_template_document", + "schema": "", + "columns": { + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "sign_template_document_document_idx": { + "name": "sign_template_document_document_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_template_document_template_fk": { + "name": "sign_template_document_template_fk", + "tableFrom": "sign_template_document", + "tableTo": "sign_template", + "columnsFrom": [ + "template_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sign_template_document_document_fk": { + "name": "sign_template_document_document_fk", + "tableFrom": "sign_template_document", + "tableTo": "sign_document", + "columnsFrom": [ + "document_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sign_template_document_template_id_position_pk": { + "name": "sign_template_document_template_id_position_pk", + "columns": [ + "template_id", + "position" + ] + } + }, + "uniqueConstraints": { + "sign_template_document_unique": { + "name": "sign_template_document_unique", + "nullsNotDistinct": false, + "columns": [ + "template_id", + "document_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sign_template_field": { + "name": "sign_template_field", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document": { + "name": "document", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sign_field_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "page": { + "name": "page", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "x": { + "name": "x", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "y": { + "name": "y", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "width": { + "name": "width", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "height": { + "name": "height", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "required": { + "name": "required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "merge_key": { + "name": "merge_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sign_template_field_template_idx": { + "name": "sign_template_field_template_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sign_template_field_template_fk": { + "name": "sign_template_field_template_fk", + "tableFrom": "sign_template_field", + "tableTo": "sign_template", + "columnsFrom": [ + "template_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_account": { + "name": "sms_account", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subaccount_sid": { + "name": "subaccount_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "auth_token": { + "name": "auth_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sms_account_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "allowed_countries": { + "name": "allowed_countries", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{US,CA}'::text[]" + }, + "unlocked_countries": { + "name": "unlocked_countries", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sms_account_organization_id_organization_id_fk": { + "name": "sms_account_organization_id_organization_id_fk", + "tableFrom": "sms_account", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_account_subaccount_sid_unique": { + "name": "sms_account_subaccount_sid_unique", + "nullsNotDistinct": false, + "columns": [ + "subaccount_sid" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_event": { + "name": "sms_event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "sms_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_status": { + "name": "provider_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "dedupe_key": { + "name": "dedupe_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_event_message_occurred_at_idx": { + "name": "sms_event_message_occurred_at_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_event_message_fk": { + "name": "sms_event_message_fk", + "tableFrom": "sms_event", + "tableTo": "sms_message", + "columnsFrom": [ + "message_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_event_message_dedupe_unique": { + "name": "sms_event_message_dedupe_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id", + "dedupe_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_inbound_receipt": { + "name": "sms_inbound_receipt", + "schema": "", + "columns": { + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_number": { + "name": "from_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_inbound_receipt_created_at_idx": { + "name": "sms_inbound_receipt_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_inbound_receipt_organization_created_at_idx": { + "name": "sms_inbound_receipt_organization_created_at_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_inbound_receipt_organization_id_organization_id_fk": { + "name": "sms_inbound_receipt_organization_id_organization_id_fk", + "tableFrom": "sms_inbound_receipt", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sms_inbound_receipt_provider_provider_message_id_pk": { + "name": "sms_inbound_receipt_provider_provider_message_id_pk", + "columns": [ + "provider", + "provider_message_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_message": { + "name": "sms_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_owner_id": { + "name": "api_key_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sms_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "from": { + "name": "from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to": { + "name": "to", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_country": { + "name": "to_country", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_id": { + "name": "number_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "discard_body": { + "name": "discard_body", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "encoding": { + "name": "encoding", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "segments": { + "name": "segments", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "traceparent": { + "name": "traceparent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_message_environment_created_at_idx": { + "name": "sms_message_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_message_organization_send_time_idx": { + "name": "sms_message_organization_send_time_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "coalesce(\"sent_at\", \"created_at\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_message_scheduled_due_idx": { + "name": "sms_message_scheduled_due_idx", + "columns": [ + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sms_message\".\"status\" = 'scheduled'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_message_queued_created_at_idx": { + "name": "sms_message_queued_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sms_message\".\"status\" = 'queued'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_message_created_at_idx": { + "name": "sms_message_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_message_environment_fk": { + "name": "sms_message_environment_fk", + "tableFrom": "sms_message", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_message_environment_idempotency_key_unique": { + "name": "sms_message_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + }, + "sms_message_id_organization_unique": { + "name": "sms_message_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_number": { + "name": "sms_number", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "sms_number_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_sid": { + "name": "provider_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "sms_number_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "registration_id": { + "name": "registration_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "receiving_environment_id": { + "name": "receiving_environment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_fee_at": { + "name": "next_fee_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_number_organization_idx": { + "name": "sms_number_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_number_next_fee_idx": { + "name": "sms_number_next_fee_idx", + "columns": [ + { + "expression": "next_fee_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sms_number\".\"next_fee_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_number_organization_id_organization_id_fk": { + "name": "sms_number_organization_id_organization_id_fk", + "tableFrom": "sms_number", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_number_phone_number_unique": { + "name": "sms_number_phone_number_unique", + "nullsNotDistinct": false, + "columns": [ + "phone_number" + ] + }, + "sms_number_id_organization_unique": { + "name": "sms_number_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_received": { + "name": "sms_received", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "number_id": { + "name": "number_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from": { + "name": "from", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to": { + "name": "to", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "segments": { + "name": "segments", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_received_environment_created_at_idx": { + "name": "sms_received_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_received_created_at_idx": { + "name": "sms_received_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_received_environment_fk": { + "name": "sms_received_environment_fk", + "tableFrom": "sms_received", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_received_provider_message_unique": { + "name": "sms_received_provider_message_unique", + "nullsNotDistinct": false, + "columns": [ + "provider", + "provider_message_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_registration": { + "name": "sms_registration", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "sms_registration_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "sms_registration_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "step": { + "name": "step", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_type": { + "name": "brand_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "profile": { + "name": "profile", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "campaign": { + "name": "campaign", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "customer_profile_sid": { + "name": "customer_profile_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trust_product_sid": { + "name": "trust_product_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_sid": { + "name": "brand_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messaging_service_sid": { + "name": "messaging_service_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "campaign_sid": { + "name": "campaign_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "toll_free_verification_sid": { + "name": "toll_free_verification_sid", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number_id": { + "name": "number_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejection_reason": { + "name": "rejection_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_fee_at": { + "name": "next_fee_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "submitted_at": { + "name": "submitted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_registration_organization_idx": { + "name": "sms_registration_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sms_registration_next_check_idx": { + "name": "sms_registration_next_check_idx", + "columns": [ + { + "expression": "next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"sms_registration\".\"next_check_at\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_registration_organization_id_organization_id_fk": { + "name": "sms_registration_organization_id_organization_id_fk", + "tableFrom": "sms_registration", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_registration_id_organization_unique": { + "name": "sms_registration_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sms_suppression": { + "name": "sms_suppression", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "sms_suppression_sequence_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone_number": { + "name": "phone_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "sms_suppression_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sms_suppression_environment_sequence_idx": { + "name": "sms_suppression_environment_sequence_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + }, + { + "expression": "sequence", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sms_suppression_environment_fk": { + "name": "sms_suppression_environment_fk", + "tableFrom": "sms_suppression", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sms_suppression_environment_phone_unique": { + "name": "sms_suppression_environment_phone_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "phone_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_attachment": { + "name": "support_attachment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uploaded_by_user_id": { + "name": "uploaded_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "support_attachment_state", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_key": { + "name": "source_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "store_key": { + "name": "store_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_attachment_message_idx": { + "name": "support_attachment_message_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_attachment_ticket_idx": { + "name": "support_attachment_ticket_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_attachment_copy_idx": { + "name": "support_attachment_copy_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_attachment\".\"state\" = 'pending_copy'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_attachment_unclaimed_idx": { + "name": "support_attachment_unclaimed_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_attachment\".\"message_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_attachment_ticket_id_support_ticket_id_fk": { + "name": "support_attachment_ticket_id_support_ticket_id_fk", + "tableFrom": "support_attachment", + "tableTo": "support_ticket", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "support_attachment_message_id_support_message_id_fk": { + "name": "support_attachment_message_id_support_message_id_fk", + "tableFrom": "support_attachment", + "tableTo": "support_message", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "support_attachment_uploaded_by_user_id_user_id_fk": { + "name": "support_attachment_uploaded_by_user_id_user_id_fk", + "tableFrom": "support_attachment", + "tableTo": "user", + "columnsFrom": [ + "uploaded_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_fake_object": { + "name": "support_fake_object", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_intake": { + "name": "support_intake", + "schema": "", + "columns": { + "received_email_id": { + "name": "received_email_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_intake_created_idx": { + "name": "support_intake_created_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_message": { + "name": "support_message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "support_message_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_data": { + "name": "event_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "author_email": { + "name": "author_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "visible_text": { + "name": "visible_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "sender_auth": { + "name": "sender_auth", + "type": "support_sender_auth", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "customer_visible": { + "name": "customer_visible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "low_signal": { + "name": "low_signal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "rfc_message_id": { + "name": "rfc_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_email_id": { + "name": "received_email_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_status": { + "name": "delivery_status", + "type": "support_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "delivery_attempts": { + "name": "delivery_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_delivery_at": { + "name": "next_delivery_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivery_error": { + "name": "delivery_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rep_alert_pending": { + "name": "rep_alert_pending", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_message_ticket_idx": { + "name": "support_message_ticket_idx", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_message_rfc_unique": { + "name": "support_message_rfc_unique", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "rfc_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"support_message\".\"rfc_message_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_message_rfc_idx": { + "name": "support_message_rfc_idx", + "columns": [ + { + "expression": "rfc_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_message\".\"rfc_message_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_message_delivery_idx": { + "name": "support_message_delivery_idx", + "columns": [ + { + "expression": "next_delivery_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_message\".\"delivery_status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_message_accepted_idx": { + "name": "support_message_accepted_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_message\".\"delivery_status\" = 'accepted'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_message_alert_idx": { + "name": "support_message_alert_idx", + "columns": [ + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_message\".\"rep_alert_pending\"", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_message_ticket_id_support_ticket_id_fk": { + "name": "support_message_ticket_id_support_ticket_id_fk", + "tableFrom": "support_message", + "tableTo": "support_ticket", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "support_message_author_user_id_user_id_fk": { + "name": "support_message_author_user_id_user_id_fk", + "tableFrom": "support_message", + "tableTo": "user", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_saved_reply": { + "name": "support_saved_reply", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_saved_reply_title_unique": { + "name": "support_saved_reply_title_unique", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_saved_reply_created_by_user_id_user_id_fk": { + "name": "support_saved_reply_created_by_user_id_user_id_fk", + "tableFrom": "support_saved_reply", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_ticket": { + "name": "support_ticket", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "number": { + "name": "number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "identity": { + "type": "always", + "name": "support_ticket_number_seq", + "schema": "public", + "increment": "1", + "startWith": "1001", + "minValue": "1", + "maxValue": "2147483647", + "cache": "1", + "cycle": false + } + }, + "reply_token": { + "name": "reply_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "severity": { + "name": "severity", + "type": "support_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "status": { + "name": "status", + "type": "support_ticket_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "source": { + "name": "source", + "type": "support_ticket_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "filter_reason": { + "name": "filter_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_name": { + "name": "organization_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_match": { + "name": "org_match", + "type": "support_org_match", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "suggested_organization_id": { + "name": "suggested_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requester_user_id": { + "name": "requester_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requester_email": { + "name": "requester_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requester_name": { + "name": "requester_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sender_auth": { + "name": "sender_auth", + "type": "support_sender_auth", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "cc": { + "name": "cc", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "suggested_cc": { + "name": "suggested_cc", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'free'" + }, + "all_hours": { + "name": "all_hours", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "awaiting_since": { + "name": "awaiting_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "response_due_at": { + "name": "response_due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "first_responded_at": { + "name": "first_responded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "breached_at": { + "name": "breached_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "risk_alerted_due_at": { + "name": "risk_alerted_due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "breach_alerted_due_at": { + "name": "breach_alerted_due_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_customer_message_at": { + "name": "last_customer_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_agent_message_at": { + "name": "last_agent_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "context": { + "name": "context", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_ticket_number_unique": { + "name": "support_ticket_number_unique", + "columns": [ + { + "expression": "number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_reply_token_unique": { + "name": "support_ticket_reply_token_unique", + "columns": [ + { + "expression": "reply_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_queue_idx": { + "name": "support_ticket_queue_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "response_due_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_organization_idx": { + "name": "support_ticket_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_message_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_suggested_idx": { + "name": "support_ticket_suggested_idx", + "columns": [ + { + "expression": "suggested_organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_requester_idx": { + "name": "support_ticket_requester_idx", + "columns": [ + { + "expression": "requester_email", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_requester_user_idx": { + "name": "support_ticket_requester_user_idx", + "columns": [ + { + "expression": "requester_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_assignee_idx": { + "name": "support_ticket_assignee_idx", + "columns": [ + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "support_ticket_snoozed_idx": { + "name": "support_ticket_snoozed_idx", + "columns": [ + { + "expression": "snoozed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"support_ticket\".\"snoozed_until\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_ticket_organization_id_organization_id_fk": { + "name": "support_ticket_organization_id_organization_id_fk", + "tableFrom": "support_ticket", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "support_ticket_suggested_organization_id_organization_id_fk": { + "name": "support_ticket_suggested_organization_id_organization_id_fk", + "tableFrom": "support_ticket", + "tableTo": "organization", + "columnsFrom": [ + "suggested_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "support_ticket_requester_user_id_user_id_fk": { + "name": "support_ticket_requester_user_id_user_id_fk", + "tableFrom": "support_ticket", + "tableTo": "user", + "columnsFrom": [ + "requester_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "support_ticket_assignee_user_id_user_id_fk": { + "name": "support_ticket_assignee_user_id_user_id_fk", + "tableFrom": "support_ticket", + "tableTo": "user", + "columnsFrom": [ + "assignee_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.support_ticket_read": { + "name": "support_ticket_read", + "schema": "", + "columns": { + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "support_ticket_read_unique": { + "name": "support_ticket_read_unique", + "columns": [ + { + "expression": "ticket_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "support_ticket_read_ticket_id_support_ticket_id_fk": { + "name": "support_ticket_read_ticket_id_support_ticket_id_fk", + "tableFrom": "support_ticket_read", + "tableTo": "support_ticket", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "support_ticket_read_user_id_user_id_fk": { + "name": "support_ticket_read_user_id_user_id_fk", + "tableFrom": "support_ticket_read", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_check": { + "name": "verify_check", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid": { + "name": "valid", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "verify_check_request_fk": { + "name": "verify_check_request_fk", + "tableFrom": "verify_check", + "tableTo": "verify_request", + "columnsFrom": [ + "request_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "verify_check_request_idempotency_key_unique": { + "name": "verify_check_request_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_request": { + "name": "verify_request", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_owner_id": { + "name": "api_key_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "channel": { + "name": "channel", + "type": "verify_channel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "to": { + "name": "to", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "to_country": { + "name": "to_country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "verify_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "failure_code": { + "name": "failure_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "risk_reasons": { + "name": "risk_reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "code_hash": { + "name": "code_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_length": { + "name": "code_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sends": { + "name": "sends", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verify_request_environment_created_at_idx": { + "name": "verify_request_environment_created_at_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verify_request_pending_to_idx": { + "name": "verify_request_pending_to_idx", + "columns": [ + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "to", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "channel", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"verify_request\".\"status\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "verify_request_created_at_idx": { + "name": "verify_request_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "verify_request_environment_fk": { + "name": "verify_request_environment_fk", + "tableFrom": "verify_request", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "verify_request_environment_idempotency_key_unique": { + "name": "verify_request_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + }, + "verify_request_id_organization_unique": { + "name": "verify_request_id_organization_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_send": { + "name": "verify_send", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_id": { + "name": "request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "verify_channel", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "route": { + "name": "route", + "type": "verify_send_route", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "sms_id": { + "name": "sms_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message_environment_id": { + "name": "message_environment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_id": { + "name": "api_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_owner_id": { + "name": "api_key_owner_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "country": { + "name": "country", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_recorded_at": { + "name": "usage_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_hash": { + "name": "request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verify_send_unmetered_idx": { + "name": "verify_send_unmetered_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"verify_send\".\"route\" = 'shared_number' and \"verify_send\".\"usage_recorded_at\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "verify_send_request_fk": { + "name": "verify_send_request_fk", + "tableFrom": "verify_send", + "tableTo": "verify_request", + "columnsFrom": [ + "request_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "verify_send_request_sequence_unique": { + "name": "verify_send_request_sequence_unique", + "nullsNotDistinct": false, + "columns": [ + "request_id", + "sequence" + ] + }, + "verify_send_environment_idempotency_key_unique": { + "name": "verify_send_environment_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id", + "idempotency_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verify_settings": { + "name": "verify_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "app_name": { + "name": "app_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code_length": { + "name": "code_length", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 6 + }, + "ttl_seconds": { + "name": "ttl_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 600 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "sms_from": { + "name": "sms_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email_from": { + "name": "email_from", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_countries": { + "name": "allowed_countries", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{US,CA}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "verify_settings_environment_fk": { + "name": "verify_settings_environment_fk", + "tableFrom": "verify_settings", + "tableTo": "environment", + "columnsFrom": [ + "environment_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "verify_settings_environment_unique": { + "name": "verify_settings_environment_unique", + "nullsNotDistinct": false, + "columns": [ + "environment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.billing_account_status": { + "name": "billing_account_status", + "schema": "public", + "values": [ + "active", + "past_due", + "delinquent" + ] + }, + "public.billing_budget_scope": { + "name": "billing_budget_scope", + "schema": "public", + "values": [ + "member_default", + "user", + "project", + "api_key" + ] + }, + "public.billing_collection_method": { + "name": "billing_collection_method", + "schema": "public", + "values": [ + "charge_automatically", + "send_invoice" + ] + }, + "public.billing_contract_kind": { + "name": "billing_contract_kind", + "schema": "public", + "values": [ + "self_serve", + "sales", + "internal" + ] + }, + "public.billing_contract_status": { + "name": "billing_contract_status", + "schema": "public", + "values": [ + "pending", + "confirmed", + "canceled" + ] + }, + "public.billing_credit_entry_kind": { + "name": "billing_credit_entry_kind", + "schema": "public", + "values": [ + "applied", + "reversed", + "voided", + "rolled_over" + ] + }, + "public.billing_credit_note_status": { + "name": "billing_credit_note_status", + "schema": "public", + "values": [ + "pending", + "issued", + "failed" + ] + }, + "public.billing_grant_applies_to": { + "name": "billing_grant_applies_to", + "schema": "public", + "values": [ + "usage", + "any" + ] + }, + "public.billing_grant_kind": { + "name": "billing_grant_kind", + "schema": "public", + "values": [ + "amount", + "units" + ] + }, + "public.billing_grant_source": { + "name": "billing_grant_source", + "schema": "public", + "values": [ + "fee", + "commitment", + "support", + "promotion", + "refund", + "sla" + ] + }, + "public.billing_hold_reason": { + "name": "billing_hold_reason", + "schema": "public", + "values": [ + "delinquent", + "spend_limit", + "admin" + ] + }, + "public.billing_invoice_kind": { + "name": "billing_invoice_kind", + "schema": "public", + "values": [ + "period", + "plan_change", + "one_off", + "final" + ] + }, + "public.billing_invoice_status": { + "name": "billing_invoice_status", + "schema": "public", + "values": [ + "draft", + "open", + "paid", + "void", + "uncollectible" + ] + }, + "public.billing_line_kind": { + "name": "billing_line_kind", + "schema": "public", + "values": [ + "plan_fee", + "proration", + "usage", + "credit", + "discount", + "commitment_fee", + "commitment_shortfall", + "add_on", + "adjustment", + "write_off" + ] + }, + "public.billing_notification_status": { + "name": "billing_notification_status", + "schema": "public", + "values": [ + "pending", + "sent", + "skipped", + "failed" + ] + }, + "public.billing_provider": { + "name": "billing_provider", + "schema": "public", + "values": [ + "fake", + "stripe", + "manual" + ] + }, + "public.billing_provider_event_status": { + "name": "billing_provider_event_status", + "schema": "public", + "values": [ + "pending", + "processed", + "ignored", + "failed" + ] + }, + "public.billing_sync_action": { + "name": "billing_sync_action", + "schema": "public", + "values": [ + "push", + "collect", + "void", + "mark_uncollectible" + ] + }, + "public.billing_tax_mode": { + "name": "billing_tax_mode", + "schema": "public", + "values": [ + "none", + "stripe_automatic" + ] + }, + "public.api_key_access": { + "name": "api_key_access", + "schema": "public", + "values": [ + "full", + "restricted" + ] + }, + "public.auth_email_kind": { + "name": "auth_email_kind", + "schema": "public", + "values": [ + "verify_email", + "reset_password", + "invitation" + ] + }, + "public.auth_email_status": { + "name": "auth_email_status", + "schema": "public", + "values": [ + "pending", + "sent", + "skipped", + "failed" + ] + }, + "public.broadcast_screening_status": { + "name": "broadcast_screening_status", + "schema": "public", + "values": [ + "pending", + "passed", + "held", + "blocked" + ] + }, + "public.broadcast_status": { + "name": "broadcast_status", + "schema": "public", + "values": [ + "draft", + "scheduled", + "sending", + "paused", + "sent", + "canceled", + "failed" + ] + }, + "public.contact_property_type": { + "name": "contact_property_type", + "schema": "public", + "values": [ + "string", + "number", + "boolean", + "date" + ] + }, + "public.contact_source": { + "name": "contact_source", + "schema": "public", + "values": [ + "api", + "dashboard", + "import", + "preference_center", + "form" + ] + }, + "public.contact_status": { + "name": "contact_status", + "schema": "public", + "values": [ + "subscribed", + "unsubscribed", + "pending" + ] + }, + "public.contact_topic_status": { + "name": "contact_topic_status", + "schema": "public", + "values": [ + "opted_in", + "opted_out" + ] + }, + "public.content_screening_verdict": { + "name": "content_screening_verdict", + "schema": "public", + "values": [ + "clean", + "flagged", + "blocked", + "error" + ] + }, + "public.dedicated_ip_pool_status": { + "name": "dedicated_ip_pool_status", + "schema": "public", + "values": [ + "waiting", + "provisioning", + "warming", + "ready", + "releasing" + ] + }, + "public.dmarc_notification_kind": { + "name": "dmarc_notification_kind", + "schema": "public", + "values": [ + "ready_quarantine", + "ready_reject", + "pass_rate_dropped" + ] + }, + "public.dmarc_notification_status": { + "name": "dmarc_notification_status", + "schema": "public", + "values": [ + "pending", + "sent", + "skipped", + "failed" + ] + }, + "public.domain_status": { + "name": "domain_status", + "schema": "public", + "values": [ + "pending", + "verified", + "temporary_failure", + "failed", + "deleting" + ] + }, + "public.email_event_type": { + "name": "email_event_type", + "schema": "public", + "values": [ + "email.sent", + "email.delivered", + "email.delivery_delayed", + "email.bounced", + "email.complained", + "email.failed", + "email.suppressed", + "email.scheduled", + "email.canceled", + "email.opened", + "email.clicked", + "email.rescheduled", + "email.unsubscribed" + ] + }, + "public.email_recipient_kind": { + "name": "email_recipient_kind", + "schema": "public", + "values": [ + "to", + "cc", + "bcc" + ] + }, + "public.email_recipient_status": { + "name": "email_recipient_status", + "schema": "public", + "values": [ + "queued", + "sent", + "delivery_delayed", + "delivered", + "bounced", + "suppressed", + "complained", + "failed", + "canceled" + ] + }, + "public.email_status": { + "name": "email_status", + "schema": "public", + "values": [ + "queued", + "sent", + "failed", + "scheduled", + "canceled" + ] + }, + "public.email_stream": { + "name": "email_stream", + "schema": "public", + "values": [ + "transactional", + "marketing" + ] + }, + "public.email_subaccount_reputation": { + "name": "email_subaccount_reputation", + "schema": "public", + "values": [ + "good", + "warning", + "paused" + ] + }, + "public.email_subaccount_status": { + "name": "email_subaccount_status", + "schema": "public", + "values": [ + "active", + "disabled", + "deleting" + ] + }, + "public.email_suppression_reason": { + "name": "email_suppression_reason", + "schema": "public", + "values": [ + "bounce", + "complaint", + "manual", + "import", + "unsubscribe" + ] + }, + "public.environment_mode": { + "name": "environment_mode", + "schema": "public", + "values": [ + "live", + "sandbox" + ] + }, + "public.hook_delivery_status": { + "name": "hook_delivery_status", + "schema": "public", + "values": [ + "pending", + "succeeded", + "failed" + ] + }, + "public.hook_recovery_status": { + "name": "hook_recovery_status", + "schema": "public", + "values": [ + "pending", + "completed" + ] + }, + "public.log_drain_batch_status": { + "name": "log_drain_batch_status", + "schema": "public", + "values": [ + "pending", + "succeeded", + "failed" + ] + }, + "public.log_drain_format": { + "name": "log_drain_format", + "schema": "public", + "values": [ + "ndjson", + "loki", + "datadog", + "otlp" + ] + }, + "public.log_drain_source": { + "name": "log_drain_source", + "schema": "public", + "values": [ + "api", + "email", + "webhook", + "risk", + "audit", + "smtp", + "sms", + "sign", + "verify", + "forms", + "scheduling" + ] + }, + "public.log_level": { + "name": "log_level", + "schema": "public", + "values": [ + "info", + "warning", + "error" + ] + }, + "public.log_source": { + "name": "log_source", + "schema": "public", + "values": [ + "api", + "email", + "webhook", + "risk", + "smtp", + "sms", + "sign", + "verify", + "forms", + "scheduling" + ] + }, + "public.mail_from_status": { + "name": "mail_from_status", + "schema": "public", + "values": [ + "pending", + "success", + "temporary_failure", + "failed" + ] + }, + "public.migration_item_kind": { + "name": "migration_item_kind", + "schema": "public", + "values": [ + "domain", + "property", + "topic", + "audience", + "template", + "webhook_endpoint" + ] + }, + "public.migration_item_outcome": { + "name": "migration_item_outcome", + "schema": "public", + "values": [ + "pending", + "created", + "existing", + "skipped", + "failed" + ] + }, + "public.migration_provider": { + "name": "migration_provider", + "schema": "public", + "values": [ + "resend", + "sendgrid", + "postmark" + ] + }, + "public.migration_status": { + "name": "migration_status", + "schema": "public", + "values": [ + "inspecting", + "ready", + "queued", + "running", + "completed", + "failed", + "canceled" + ] + }, + "public.org_audit_actor_type": { + "name": "org_audit_actor_type", + "schema": "public", + "values": [ + "user", + "api_key", + "system" + ] + }, + "public.organization_deletion_status": { + "name": "organization_deletion_status", + "schema": "public", + "values": [ + "pending", + "completed" + ] + }, + "public.organization_logo_status": { + "name": "organization_logo_status", + "schema": "public", + "values": [ + "found", + "not_found", + "hidden" + ] + }, + "public.received_email_status": { + "name": "received_email_status", + "schema": "public", + "values": [ + "received", + "blocked" + ] + }, + "public.reputation_actor": { + "name": "reputation_actor", + "schema": "public", + "values": [ + "system", + "admin" + ] + }, + "public.reputation_event_type": { + "name": "reputation_event_type", + "schema": "public", + "values": [ + "warning_raised", + "warning_cleared", + "suspended", + "reinstated", + "policy_updated", + "marketing_paused", + "marketing_reinstated" + ] + }, + "public.reputation_notification_status": { + "name": "reputation_notification_status", + "schema": "public", + "values": [ + "pending", + "sent", + "skipped", + "failed" + ] + }, + "public.reputation_status": { + "name": "reputation_status", + "schema": "public", + "values": [ + "good", + "warning", + "suspended" + ] + }, + "public.risk_captcha_provider": { + "name": "risk_captcha_provider", + "schema": "public", + "values": [ + "turnstile", + "hcaptcha" + ] + }, + "public.risk_decision": { + "name": "risk_decision", + "schema": "public", + "values": [ + "allow", + "review", + "block" + ] + }, + "public.risk_event": { + "name": "risk_event", + "schema": "public", + "values": [ + "signup", + "login", + "message", + "checkout", + "custom" + ] + }, + "public.risk_feedback_source": { + "name": "risk_feedback_source", + "schema": "public", + "values": [ + "api", + "dashboard" + ] + }, + "public.risk_identity_kind": { + "name": "risk_identity_kind", + "schema": "public", + "values": [ + "mailbox", + "device" + ] + }, + "public.risk_limit_failure_mode": { + "name": "risk_limit_failure_mode", + "schema": "public", + "values": [ + "allow", + "deny" + ] + }, + "public.risk_list": { + "name": "risk_list", + "schema": "public", + "values": [ + "allow", + "block" + ] + }, + "public.risk_list_entry_type": { + "name": "risk_list_entry_type", + "schema": "public", + "values": [ + "email", + "domain", + "ip", + "asn", + "country", + "user" + ] + }, + "public.risk_outcome": { + "name": "risk_outcome", + "schema": "public", + "values": [ + "legitimate", + "fraud", + "abuse" + ] + }, + "public.risk_report_kind": { + "name": "risk_report_kind", + "schema": "public", + "values": [ + "mailbox", + "ip", + "subnet", + "user", + "card", + "device" + ] + }, + "public.risk_review_status": { + "name": "risk_review_status", + "schema": "public", + "values": [ + "open", + "resolved" + ] + }, + "public.risk_rule_match": { + "name": "risk_rule_match", + "schema": "public", + "values": [ + "all", + "any" + ] + }, + "public.risk_user_network_kind": { + "name": "risk_user_network_kind", + "schema": "public", + "values": [ + "country", + "asn" + ] + }, + "public.tenant_sending_status": { + "name": "tenant_sending_status", + "schema": "public", + "values": [ + "enabled", + "disabled", + "reinstated" + ] + }, + "public.topic_default_subscription": { + "name": "topic_default_subscription", + "schema": "public", + "values": [ + "opt_in", + "opt_out" + ] + }, + "public.topic_visibility": { + "name": "topic_visibility", + "schema": "public", + "values": [ + "public", + "private" + ] + }, + "public.tracking_domain_status": { + "name": "tracking_domain_status", + "schema": "public", + "values": [ + "pending", + "active", + "temporary_failure", + "failed", + "deleting" + ] + }, + "public.webhook_delivery_status": { + "name": "webhook_delivery_status", + "schema": "public", + "values": [ + "pending", + "succeeded", + "failed" + ] + }, + "public.form_captcha": { + "name": "form_captcha", + "schema": "public", + "values": [ + "off", + "required" + ] + }, + "public.form_delivery_kind": { + "name": "form_delivery_kind", + "schema": "public", + "values": [ + "notification", + "contact", + "verification" + ] + }, + "public.form_delivery_state": { + "name": "form_delivery_state", + "schema": "public", + "values": [ + "pending", + "delivered", + "failed", + "skipped" + ] + }, + "public.form_on_review": { + "name": "form_on_review", + "schema": "public", + "values": [ + "deliver", + "hold" + ] + }, + "public.form_spam_reason": { + "name": "form_spam_reason", + "schema": "public", + "values": [ + "honeypot", + "risk_block", + "risk_review", + "content" + ] + }, + "public.form_submission_status": { + "name": "form_submission_status", + "schema": "public", + "values": [ + "pending_confirmation", + "accepted", + "held", + "spam" + ] + }, + "public.inbox_kind": { + "name": "inbox_kind", + "schema": "public", + "values": [ + "managed", + "domain" + ] + }, + "public.inbox_message_direction": { + "name": "inbox_message_direction", + "schema": "public", + "values": [ + "inbound", + "outbound" + ] + }, + "public.notification_status": { + "name": "notification_status", + "schema": "public", + "values": [ + "pending", + "sent", + "skipped", + "failed" + ] + }, + "public.apns_environment": { + "name": "apns_environment", + "schema": "public", + "values": [ + "production", + "sandbox" + ] + }, + "public.push_notification_kind": { + "name": "push_notification_kind", + "schema": "public", + "values": [ + "reputation_warning", + "reputation_suspended", + "billing_payment_failed", + "billing_hold_placed", + "billing_budget_paused", + "webhook_endpoint_disabled", + "hook_endpoint_disabled", + "log_drain_disabled", + "risk_review" + ] + }, + "public.push_notification_status": { + "name": "push_notification_status", + "schema": "public", + "values": [ + "pending", + "sent", + "skipped", + "failed" + ] + }, + "public.scheduling_attendee_role": { + "name": "scheduling_attendee_role", + "schema": "public", + "values": [ + "attendee", + "guest" + ] + }, + "public.scheduling_booking_status": { + "name": "scheduling_booking_status", + "schema": "public", + "values": [ + "held", + "pending_approval", + "confirmed", + "cancelled", + "declined" + ] + }, + "public.scheduling_calendar_details": { + "name": "scheduling_calendar_details", + "schema": "public", + "values": [ + "neutral", + "full" + ] + }, + "public.scheduling_calendar_event_state": { + "name": "scheduling_calendar_event_state", + "schema": "public", + "values": [ + "present", + "absent" + ] + }, + "public.scheduling_calendar_failure_mode": { + "name": "scheduling_calendar_failure_mode", + "schema": "public", + "values": [ + "hide", + "last_known" + ] + }, + "public.scheduling_calendar_provider": { + "name": "scheduling_calendar_provider", + "schema": "public", + "values": [ + "google", + "microsoft", + "caldav", + "simulated", + "zoom" + ] + }, + "public.scheduling_connection_status": { + "name": "scheduling_connection_status", + "schema": "public", + "values": [ + "active", + "needs_reauth" + ] + }, + "public.scheduling_event_type_kind": { + "name": "scheduling_event_type_kind", + "schema": "public", + "values": [ + "one_on_one", + "pool", + "collective", + "group" + ] + }, + "public.scheduling_event_type_status": { + "name": "scheduling_event_type_status", + "schema": "public", + "values": [ + "active", + "archived" + ] + }, + "public.scheduling_external_state": { + "name": "scheduling_external_state", + "schema": "public", + "values": [ + "in_sync", + "moved", + "deleted" + ] + }, + "public.scheduling_host_role": { + "name": "scheduling_host_role", + "schema": "public", + "values": [ + "required", + "member" + ] + }, + "public.scheduling_host_status": { + "name": "scheduling_host_status", + "schema": "public", + "values": [ + "active", + "disabled" + ] + }, + "public.scheduling_notification_channel": { + "name": "scheduling_notification_channel", + "schema": "public", + "values": [ + "email", + "sms" + ] + }, + "public.scheduling_notification_recipient": { + "name": "scheduling_notification_recipient", + "schema": "public", + "values": [ + "attendee", + "host" + ] + }, + "public.scheduling_session_kind": { + "name": "scheduling_session_kind", + "schema": "public", + "values": [ + "host", + "booker", + "manage" + ] + }, + "public.sign_authentication": { + "name": "sign_authentication", + "schema": "public", + "values": [ + "email_link", + "customer_asserted" + ] + }, + "public.sign_delivery": { + "name": "sign_delivery", + "schema": "public", + "values": [ + "embedded", + "email" + ] + }, + "public.sign_event_type": { + "name": "sign_event_type", + "schema": "public", + "values": [ + "created", + "sent", + "notified", + "viewed", + "consented", + "signed", + "declined", + "reminded", + "canceled", + "expired", + "completed", + "sealed" + ] + }, + "public.sign_field_type": { + "name": "sign_field_type", + "schema": "public", + "values": [ + "signature", + "initials", + "date_signed", + "name", + "email", + "text", + "checkbox" + ] + }, + "public.sign_request_status": { + "name": "sign_request_status", + "schema": "public", + "values": [ + "draft", + "sent", + "completed", + "declined", + "canceled", + "expired" + ] + }, + "public.sign_signer_status": { + "name": "sign_signer_status", + "schema": "public", + "values": [ + "pending", + "notified", + "viewed", + "signed", + "declined" + ] + }, + "public.sign_signing_order": { + "name": "sign_signing_order", + "schema": "public", + "values": [ + "parallel", + "sequential" + ] + }, + "public.sms_account_status": { + "name": "sms_account_status", + "schema": "public", + "values": [ + "active", + "closing", + "closed" + ] + }, + "public.sms_event_type": { + "name": "sms_event_type", + "schema": "public", + "values": [ + "sms.scheduled", + "sms.sent", + "sms.delivered", + "sms.undelivered", + "sms.failed", + "sms.suppressed", + "sms.canceled" + ] + }, + "public.sms_number_kind": { + "name": "sms_number_kind", + "schema": "public", + "values": [ + "local", + "toll_free" + ] + }, + "public.sms_number_status": { + "name": "sms_number_status", + "schema": "public", + "values": [ + "pending", + "active", + "releasing" + ] + }, + "public.sms_registration_kind": { + "name": "sms_registration_kind", + "schema": "public", + "values": [ + "us_10dlc", + "toll_free" + ] + }, + "public.sms_registration_status": { + "name": "sms_registration_status", + "schema": "public", + "values": [ + "draft", + "submitted", + "in_review", + "approved", + "rejected", + "failed" + ] + }, + "public.sms_status": { + "name": "sms_status", + "schema": "public", + "values": [ + "queued", + "scheduled", + "sent", + "delivered", + "undelivered", + "failed", + "canceled" + ] + }, + "public.sms_suppression_reason": { + "name": "sms_suppression_reason", + "schema": "public", + "values": [ + "opt_out", + "manual", + "invalid_number" + ] + }, + "public.support_attachment_state": { + "name": "support_attachment_state", + "schema": "public", + "values": [ + "uploaded", + "pending_copy", + "stored", + "failed" + ] + }, + "public.support_delivery_status": { + "name": "support_delivery_status", + "schema": "public", + "values": [ + "pending", + "accepted", + "delivered", + "bounced", + "failed" + ] + }, + "public.support_message_kind": { + "name": "support_message_kind", + "schema": "public", + "values": [ + "customer", + "agent", + "note", + "event" + ] + }, + "public.support_org_match": { + "name": "support_org_match", + "schema": "public", + "values": [ + "session", + "email", + "manual" + ] + }, + "public.support_sender_auth": { + "name": "support_sender_auth", + "schema": "public", + "values": [ + "session", + "dmarc_pass", + "unverified" + ] + }, + "public.support_severity": { + "name": "support_severity", + "schema": "public", + "values": [ + "normal", + "urgent" + ] + }, + "public.support_ticket_source": { + "name": "support_ticket_source", + "schema": "public", + "values": [ + "email", + "dashboard" + ] + }, + "public.support_ticket_status": { + "name": "support_ticket_status", + "schema": "public", + "values": [ + "open", + "pending", + "snoozed", + "resolved", + "spam" + ] + }, + "public.verify_channel": { + "name": "verify_channel", + "schema": "public", + "values": [ + "sms", + "email" + ] + }, + "public.verify_send_route": { + "name": "verify_send_route", + "schema": "public", + "values": [ + "own_number", + "shared_number", + "email", + "coss_email" + ] + }, + "public.verify_status": { + "name": "verify_status", + "schema": "public", + "values": [ + "pending", + "approved", + "expired", + "canceled", + "failed", + "blocked" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 7bf753d90..7f8404a90 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -897,6 +897,13 @@ "when": 1793815138250, "tag": "0127_sudden_shooting_star", "breakpoints": true + }, + { + "idx": 128, + "version": "7", + "when": 1793824967951, + "tag": "0128_chemical_echo", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/scheduling.ts b/packages/db/src/schema/scheduling.ts index 37146934e..a6deea2fb 100644 --- a/packages/db/src/schema/scheduling.ts +++ b/packages/db/src/schema/scheduling.ts @@ -169,6 +169,8 @@ export const schedulingHost = pgTable( locations: jsonb("locations").$type().notNull().default([]), status: schedulingHostStatus("status").notNull().default("active"), screenedAt: instant("screened_at"), + // Live hosts: when the next day's `scheduling.hosts` is due. Sandbox ones are never charged. + nextFeeAt: instant("next_fee_at"), createdAt: createdAt(), updatedAt: updatedAt(), }, @@ -182,6 +184,10 @@ export const schedulingHost = pgTable( unique("scheduling_host_id_organization_unique").on(t.id, t.organizationId), index("scheduling_host_environment_created_at_idx").on(t.environmentId, t.createdAt, t.id), index("scheduling_host_attributes_idx").using("gin", t.attributes.op("jsonb_path_ops")), + index("scheduling_host_next_fee_at_idx").on(t.nextFeeAt).where(sql`${t.nextFeeAt} IS NOT NULL`), + index("scheduling_host_live_organization_idx") + .on(t.organizationId) + .where(sql`${t.nextFeeAt} IS NOT NULL`), ], ); diff --git a/packages/email/src/errors.ts b/packages/email/src/errors.ts index d6bef65fe..681753c67 100644 --- a/packages/email/src/errors.ts +++ b/packages/email/src/errors.ts @@ -157,6 +157,8 @@ export class ContactError extends Error { } export type InboxErrorCode = + | "billing_hold" + | "budget_exceeded" | "not_found" | "thread_not_found" | "message_not_found" diff --git a/packages/email/src/inboxes.ts b/packages/email/src/inboxes.ts index 53418640f..fe3153512 100644 --- a/packages/email/src/inboxes.ts +++ b/packages/email/src/inboxes.ts @@ -19,7 +19,13 @@ */ import { createHash, randomInt } from "node:crypto"; import type { PlanGate, UsageGate } from "@coss/billing/gate"; -import { recordUsage } from "@coss/billing/metering"; +import { + BUDGET_EXCEEDED_MESSAGES, + budgetHoldFor, + countTowardAllowance, + isOnBillingHold, + recordUsage, +} from "@coss/billing/metering"; import { db, type Executor, schema, type Transaction } from "@coss/db"; import { ID_PREFIXES, newId } from "@coss/db/id"; import { enqueueWebhookMessage } from "@coss/webhooks"; @@ -226,6 +232,21 @@ export async function createInbox( const existing = await findByClientId(env, input.clientId); if (existing) return { inbox: toInboxObject(existing, sandbox), created: false }; } + // A new inbox is a new monthly charge. Those that exist keep receiving: mail is never lost to a hold. + if (!sandbox) { + if (await isOnBillingHold(env.organizationId)) { + throw new InboxError( + "billing_hold", + "New live inboxes are paused: your organization has an overdue invoice or reached its " + + "spend limit. Sandbox keys still work.", + ); + } + const held = await budgetHoldFor({ + organizationId: env.organizationId, + projectId: env.projectId, + }); + if (held) throw new InboxError("budget_exceeded", BUDGET_EXCEEDED_MESSAGES[held]); + } const target = await resolveDomain(env, input.domain, deps.managedDomain); for (let attempt = 0; attempt < 3; attempt++) { @@ -350,33 +371,19 @@ export async function chargeDueInboxFees( byEnvironment.set(row.environmentId, entry); } for (const { source, days } of byEnvironment.values()) { - if (opts.billing) await countTowardAllowance(tx, opts.billing, source.organizationId, days); + if (opts.billing) { + await countTowardAllowance(tx, opts.billing, { + organizationId: source.organizationId, + meter: "email.inboxes", + days, + }); + } await recordUsage(tx, { ...source, meter: "email.inboxes", quantity: days }); } return charged.length; }); } -/** Reserves `days` inbox-days one by one (each in a savepoint) until the allowance refuses one. */ -async function countTowardAllowance( - tx: Transaction, - gate: UsageGate, - organizationId: string, - days: number, -): Promise { - for (let day = 0; day < days; day++) { - try { - await tx.transaction((savepoint) => - gate.reserve(savepoint, { organizationId, meter: "email.inboxes", quantity: 1 }), - ); - } catch (error) { - const code = (error as { code?: string } | null)?.code; - if (code === "usage_limit_exceeded" || code === "plan_feature_unavailable") return; - throw error; - } - } -} - export async function getInboxRow(scope: Scope & { id: string }): Promise { const [row] = await db .select() @@ -458,7 +465,11 @@ export async function deleteInbox( if (!row) return false; const { dueDays, ...source } = row; if (deps.billing && dueDays > 0) { - await countTowardAllowance(tx, deps.billing, source.organizationId, dueDays); + await countTowardAllowance(tx, deps.billing, { + organizationId: source.organizationId, + meter: "email.inboxes", + days: dueDays, + }); } await recordUsage(tx, { ...source, meter: "email.inboxes", quantity: dueDays }); return true; diff --git a/packages/email/test/contacts.test.ts b/packages/email/test/contacts.test.ts index 7b1bafe18..cbc9023df 100644 --- a/packages/email/test/contacts.test.ts +++ b/packages/email/test/contacts.test.ts @@ -61,6 +61,9 @@ async function upsert(env: ReturnType, input: Record { const row = await acceptingForm(input.formId); if (!row) return { outcome: "unavailable" }; + // A live submission is usage. The submitter sees what a closed form shows, nothing of the + // customer's billing. + if ( + row.mode === "live" && + ((await isOnBillingHold(row.organizationId)) || + (await budgetHoldFor({ organizationId: row.organizationId, projectId: row.projectId }))) + ) { + return { outcome: "unavailable" }; + } const shown: SubmittedForm = { id: row.id, name: row.name, diff --git a/packages/migrate/test/runner.test.ts b/packages/migrate/test/runner.test.ts index 5c1a06222..acebe2d68 100644 --- a/packages/migrate/test/runner.test.ts +++ b/packages/migrate/test/runner.test.ts @@ -414,6 +414,9 @@ describe("runDueMigrations", () => { const env = envOf(tenant); const plans: PlanGate = { async requireFeature() {}, + async hasFeature() { + return true; + }, async limit() { return 1; }, diff --git a/packages/ratelimit/src/buckets.ts b/packages/ratelimit/src/buckets.ts index 448b57dc9..61da4b750 100644 --- a/packages/ratelimit/src/buckets.ts +++ b/packages/ratelimit/src/buckets.ts @@ -479,6 +479,16 @@ export const BUCKETS = { // Twilio's status and inbound callbacks (`/sms/twilio/*`): signatures are checked before // anything is stored, so this only bounds the work forged requests cause. A store outage allows // rather than drops delivery reports and replies. + // Live slot lists and availability searches, per organization: the plan sets the rate + // (`scheduling.slots.second` in `@coss/billing/catalog`); this is Free's. + "scheduling.slots.second": { + scope: "org", + limit: 5, + periodMs: SECOND, + burst: 20, + overridable: true, + failureMode: "allow", + }, // A browser session's calls, per session, and per address before the token is even looked up. "scheduling.session": { scope: "key", diff --git a/packages/scheduling/AGENTS.md b/packages/scheduling/AGENTS.md index a48ca3a41..4038e8fd7 100644 --- a/packages/scheduling/AGENTS.md +++ b/packages/scheduling/AGENTS.md @@ -194,6 +194,33 @@ One `scheduling.bookings` unit per confirmed booking, series occurrence or group on `usage_recorded_at` (`recordUsage` doesn't dedupe). `reserve` runs after a host has won. Holds and pending approvals aren't charged; reschedules are free; cancelled stays charged. +`scheduling.hosts` is one unit per day a live host exists (`next_fee_at`, its presence is what makes a host live for +the plan's count): on creation, then `chargeDueHostFees` (the worker's `scheduling-host-fees`), and `deleteHost` charges +what's due. Nothing is recorded before `meterPricedFrom` (2027-01-01): hosts wait with `next_fee_at` at that date. + +## Plans and holds + +`plan.ts`. **Live only**: sandbox has everything on every plan, so a team builds first and meets the plan at go-live. +Injected as `SchedulingDeps.plans`; without it nothing is enforced. What the plan decides: + +- `schedulingHosts` (`createHost`, across live environments, under an advisory lock). Free and Pro stop; Scale has no + limit and is charged past what it includes. +- `schedulingHolds` (`createHold`), `schedulingHostPortal` (`createHostSession` from the API only: a dashboard member's + own look passes no gate, and host invitation emails stay on every plan), `schedulingRouting` (`host_filter`, and + `attributes` at the API's routes, never in `book()`: a session's were checked when it was made). +- `schedulingGroupSeats` (growing a `capacity`) and `schedulingSearchHosts` (`maxHosts`: a search looks at that many and + says `has_more`, it never fails). +- `schedulingWhiteLabel` (`coss_attribution` in the client API's `/session`). +- `scheduling.slots.second` (`limitSchedulingSlots` in the API): live slot lists and searches per organization. + +A person on a hosted page never meets a plan gate (Free's booking allowance aside): a session's attributes aren't +checked again. Never gate the double-booking guarantee, calendars or series. A filter or a group size that exists keeps working whatever the plan; a downgrade is refused only for live +`host_filter`s (the dashboard's preflight). + +`refuseNewBookingWhileHeld`: a billing hold or the project's budget stops new live bookings, holds and series (402), and +the booker's page says only that the account can't take bookings. Confirming a hold and approving keep working: the +customer may have taken payment for them. + ## Testing `test/bookings.test.ts` races on real Postgres. `test/engine-differential.test.ts` compares `buildAvailability` with diff --git a/packages/scheduling/src/availability.ts b/packages/scheduling/src/availability.ts index ad2a8682a..a6647ffd5 100644 --- a/packages/scheduling/src/availability.ts +++ b/packages/scheduling/src/availability.ts @@ -574,6 +574,8 @@ export async function searchAvailability( hostRefs?: readonly string[] | undefined; slotsPerHost: number; limit: number; + /** The plan's hosts per search: beyond them, `hasMore` says there's more than was searched. */ + maxHosts?: number | null | undefined; }, ): Promise<{ data: { hostId: string; uid: string | null; slots: { startMs: number; endMs: number }[] }[]; @@ -586,7 +588,9 @@ export async function searchAvailability( attributes: input.attributes, hostRefs: input.hostRefs, }); - const entries = candidates.members; + const entries = + input.maxHosts != null ? candidates.members.slice(0, input.maxHosts) : candidates.members; + const cutShort = entries.length < candidates.members.length; const loaded = await loadHostInputs(db, { scope, hosts: entries.map((entry) => ({ @@ -628,5 +632,5 @@ export async function searchAvailability( }); } rows.sort((a, b) => (a.slots[0]?.startMs ?? 0) - (b.slots[0]?.startMs ?? 0)); - return { data: rows.slice(0, input.limit), hasMore: rows.length > input.limit }; + return { data: rows.slice(0, input.limit), hasMore: cutShort || rows.length > input.limit }; } diff --git a/packages/scheduling/src/bookings.ts b/packages/scheduling/src/bookings.ts index 990e71831..bcb43e10f 100644 --- a/packages/scheduling/src/bookings.ts +++ b/packages/scheduling/src/bookings.ts @@ -75,6 +75,7 @@ import { type EventTypeRow, resolveEventType } from "./event-types.ts"; import { confirmedSeats, withSessionLocation } from "./group-sessions.ts"; import { checkTimeZone, type HostRow, hostsByIds, resolveHost } from "./hosts.ts"; import { carryReminders, enqueueBookingNotifications } from "./notifications.ts"; +import { refuseNewBookingWhileHeld, requireLiveFeature } from "./plan.ts"; import { type AttendeeInputOutput, type BookingObjectOutput, @@ -1425,6 +1426,7 @@ async function book( actor: BookingActor, wantHold: boolean, ): Promise<{ booking: BookingObjectOutput; replayed: boolean }> { + await refuseNewBookingWhileHeld(env); checkStart(request.startMs); checkAttendee(request.attendee); const eventType = await resolveEventType(db, env, request.eventType); @@ -1524,7 +1526,10 @@ export const createHold = ( request: BookingRequest, deps: SchedulingDeps, actor: BookingActor, -) => book(env, request, deps, actor, true); +) => + requireLiveFeature(env, deps.plans, "schedulingHolds").then(() => + book(env, request, deps, actor, true), + ); async function lockBooking(tx: Executor, scope: Scope, id: string): Promise { if (!isId(ID_PREFIXES.schedulingBooking, id)) { @@ -2706,6 +2711,7 @@ export async function createSeries( deps: SchedulingDeps, actor: BookingActor, ): Promise<{ result: SeriesResult; replayed: boolean }> { + await refuseNewBookingWhileHeld(env); checkStart(request.startMs); checkAttendee(request.attendee); const eventType = await resolveEventType(db, env, request.eventType); diff --git a/packages/scheduling/src/errors.ts b/packages/scheduling/src/errors.ts index 8f99e4c8b..666cdc417 100644 --- a/packages/scheduling/src/errors.ts +++ b/packages/scheduling/src/errors.ts @@ -18,6 +18,8 @@ export type SchedulingErrorCode = | "calendar_unavailable" | "scheduling_unavailable" | "account_suspended" + | "billing_hold" + | "budget_exceeded" | "idempotency_in_progress" | "idempotency_key_reused"; @@ -42,6 +44,8 @@ const STATUS_BY_CODE = { calendar_unavailable: 422, scheduling_unavailable: 503, account_suspended: 403, + billing_hold: 402, + budget_exceeded: 402, idempotency_in_progress: 409, idempotency_key_reused: 422, } as const satisfies Record; diff --git a/packages/scheduling/src/event-types.ts b/packages/scheduling/src/event-types.ts index 7d5678564..21c8e92cf 100644 --- a/packages/scheduling/src/event-types.ts +++ b/packages/scheduling/src/event-types.ts @@ -1,11 +1,26 @@ /** What can be booked. Bookings snapshot what they occupy, so edits never move an existing one. */ +import type { PlanGate } from "@coss/billing/gate"; import { HipaaModeError, hipaaModeApplies } from "@coss/compliance"; import { db, type Executor, schema } from "@coss/db"; import { ID_PREFIXES, isId } from "@coss/db/id"; -import { and, asc, desc, eq, gt, inArray, isNull, lt, notInArray, or, sql } from "drizzle-orm"; +import { + and, + asc, + desc, + eq, + gt, + inArray, + isNotNull, + isNull, + lt, + notInArray, + or, + sql, +} from "drizzle-orm"; import { notZoomMeeting } from "./calendar/zoom.ts"; import { SchedulingError } from "./errors.ts"; import { checkTimeZone, type HostRow, resolveHost } from "./hosts.ts"; +import { requireRouting } from "./plan.ts"; import type { CreateEventTypeRequestOutput, EventTypeObjectOutput, @@ -347,11 +362,47 @@ async function rewriteUpcomingEvents(tx: Executor, row: EventTypeRow): Promise { + const [row] = await db + .select({ id: schedulingEventType.id }) + .from(schedulingEventType) + .innerJoin( + schema.environment, + and( + eq(schema.environment.id, schedulingEventType.environmentId), + eq(schema.environment.organizationId, schedulingEventType.organizationId), + ), + ) + .where( + and( + eq(schedulingEventType.organizationId, organizationId), + eq(schema.environment.mode, "live"), + isNotNull(schedulingEventType.hostFilter), + ), + ) + .limit(1); + return Boolean(row); +} + +/** A live group session's seats, within the plan's limit (one that shrinks is never refused). */ +async function checkGroupSeats( + env: { organizationId: string; mode?: "live" | "sandbox" }, + plans: PlanGate | undefined, + capacity: number | null | undefined, + existing = 0, +): Promise { + if (env.mode !== "live" || !capacity || capacity <= existing) return; + await plans?.requireCapacity(env.organizationId, "schedulingGroupSeats", 0, capacity); +} + export async function createEventType( env: SchedulingEnvironment, params: CreateEventTypeRequestOutput, - opts: { screen?: SetupScreening | undefined } = {}, + opts: { screen?: SetupScreening | undefined; plans?: PlanGate | undefined } = {}, ): Promise { + await requireRouting(env, opts.plans, params.host_filter); + await checkGroupSeats(env, opts.plans, params.capacity); refuseLinks(params.title, "title"); refuseLinksInAddresses(params.locations); await screenSetup(env, setupText(params), opts.screen, "title"); @@ -500,7 +551,7 @@ export async function updateEventType( scope: Scope & { mode?: "live" | "sandbox" }, ref: string, params: UpdateEventTypeRequestOutput, - opts: { screen?: SetupScreening | undefined } = {}, + opts: { screen?: SetupScreening | undefined; plans?: PlanGate | undefined } = {}, ): Promise { refuseLinks(params.title, "title"); refuseLinksInAddresses(params.locations); @@ -524,6 +575,13 @@ export async function updateEventType( try { return await db.transaction(async (tx) => { const existing = await resolveEventType(tx, scope, ref); + // What it already has keeps working: only a change meets the plan. + if (JSON.stringify(params.host_filter ?? null) !== JSON.stringify(existing.hostFilter)) { + if (params.host_filter !== undefined) { + await requireRouting(scope, opts.plans, params.host_filter); + } + } + await checkGroupSeats(scope, opts.plans, params.capacity, existing.capacity ?? 0); const set = { ...(params.uid !== undefined ? { uid: params.uid } : {}), ...(params.title !== undefined ? { title: params.title } : {}), diff --git a/packages/scheduling/src/hosts.ts b/packages/scheduling/src/hosts.ts index c937947b6..fa6c19b40 100644 --- a/packages/scheduling/src/hosts.ts +++ b/packages/scheduling/src/hosts.ts @@ -2,9 +2,25 @@ * Hosts: the people who can be booked. Every function takes the environment and scopes its SQL by * the organization (tenancy rule). */ +import { meterPricedFrom } from "@coss/billing/catalog"; +import type { PlanGate, UsageGate } from "@coss/billing/gate"; +import { countTowardAllowance, recordUsage } from "@coss/billing/metering"; import { db, type Executor, schema } from "@coss/db"; import { ID_PREFIXES, isId } from "@coss/db/id"; -import { and, desc, eq, inArray, isNull, lt, or, sql } from "drizzle-orm"; +import { + and, + asc, + count, + desc, + eq, + inArray, + isNotNull, + isNull, + lt, + lte, + or, + sql, +} from "drizzle-orm"; import { isValidTimeZone } from "./engine/time.ts"; import { SchedulingError } from "./errors.ts"; import type { @@ -77,6 +93,17 @@ function hostMatch(scope: Scope, ref: string) { ); } +/** Live hosts of the organization, across its environments: what the plan's limit counts. */ +export async function countLiveHosts(organizationId: string): Promise { + const [total] = await db + .select({ value: count() }) + .from(schedulingHost) + .where( + and(eq(schedulingHost.organizationId, organizationId), isNotNull(schedulingHost.nextFeeAt)), + ); + return total?.value ?? 0; +} + /** A host by its id or `uid`, or `not_found`. */ export async function resolveHost(executor: Executor, scope: Scope, ref: string): Promise { const [row] = await executor.select().from(schedulingHost).where(hostMatch(scope, ref)).limit(1); @@ -94,33 +121,89 @@ const uidTaken = (uid: string | null | undefined) => param: "uid", }); +const HOST_FEE_METER = "scheduling.hosts"; +const HOST_FEE_INTERVAL_MS = 24 * 60 * 60 * 1000; + +/** + * Creates a host. A live one counts against the plan's `schedulingHosts` (across the organization's + * live environments, under an organization-wide lock) and is charged by the day from its creation, + * or from when the meter is first priced; the worker charges the days after. + */ export async function createHost( env: SchedulingEnvironment, params: CreateHostRequestOutput, - opts: { screen?: SetupScreening | undefined } = {}, + opts: { + screen?: SetupScreening | undefined; + plans?: PlanGate | undefined; + billing?: UsageGate | undefined; + now?: Date; + } = {}, ): Promise { checkTimeZone(params.time_zone, "time_zone"); refuseLinks(params.name, "name"); refuseLinksInAddresses(params.locations); await screenSetup(env, params.name, opts.screen, "name"); await screenLocations(env, params.locations, opts.screen); + const live = env.mode === "live"; + const now = opts.now ?? new Date(); + const pricedFrom = meterPricedFrom(HOST_FEE_METER) ?? now; + const chargedNow = live && pricedFrom <= now; try { - const [row] = await db - .insert(schedulingHost) - .values({ - organizationId: env.organizationId, - projectId: env.projectId, - environmentId: env.environmentId, - uid: params.uid ?? null, - name: params.name, - email: params.email ?? null, - timeZone: params.time_zone, - attributes: params.attributes ?? {}, - locations: params.locations ?? [], - metadata: params.metadata ?? {}, - }) - .returning(); - return toHostObject(row as HostRow); + return await db.transaction(async (tx) => { + if (live && opts.plans) { + // One creation at a time per organization, so the count holds. + await tx.execute( + sql`select pg_advisory_xact_lock(hashtext(${`scheduling-hosts:${env.organizationId}`}))`, + ); + const [total] = await tx + .select({ value: count() }) + .from(schedulingHost) + .where( + and( + eq(schedulingHost.organizationId, env.organizationId), + isNotNull(schedulingHost.nextFeeAt), + ), + ); + await opts.plans.requireCapacity(env.organizationId, "schedulingHosts", total?.value ?? 0); + } + const [row] = await tx + .insert(schedulingHost) + .values({ + organizationId: env.organizationId, + projectId: env.projectId, + environmentId: env.environmentId, + uid: params.uid ?? null, + name: params.name, + email: params.email ?? null, + timeZone: params.time_zone, + attributes: params.attributes ?? {}, + locations: params.locations ?? [], + metadata: params.metadata ?? {}, + nextFeeAt: !live + ? null + : chargedNow + ? new Date(now.getTime() + HOST_FEE_INTERVAL_MS) + : pricedFrom, + }) + .returning(); + if (chargedNow) { + if (opts.billing) { + await countTowardAllowance(tx, opts.billing, { + organizationId: env.organizationId, + meter: HOST_FEE_METER, + days: 1, + }); + } + await recordUsage(tx, { + organizationId: env.organizationId, + environmentId: env.environmentId, + projectId: env.projectId, + meter: HOST_FEE_METER, + quantity: 1, + }); + } + return toHostObject(row as HostRow); + }); } catch (error) { if (isUniqueViolation(error)) throw uidTaken(params.uid); throw error; @@ -204,7 +287,11 @@ export async function updateHost( } /** A host with bookings still to come can't go: disabling it keeps them and stops new ones. */ -export async function deleteHost(scope: Scope, ref: string): Promise<{ id: string }> { +export async function deleteHost( + scope: Scope, + ref: string, + opts: { billing?: UsageGate | undefined } = {}, +): Promise<{ id: string }> { return db.transaction(async (tx) => { const host = await resolveHost(tx, scope, ref); await tx @@ -246,15 +333,100 @@ export async function deleteHost(scope: Scope, ref: string): Promise<{ id: strin "This host has upcoming bookings or holds. Cancel them, or set the host's status to disabled.", ); } - await tx + const [deleted] = await tx .delete(schedulingHost) .where( and(eq(schedulingHost.id, host.id), eq(schedulingHost.organizationId, host.organizationId)), - ); + ) + .returning({ + // Days that started since its last charge: the fee loop hadn't reached them. + dueDays: sql`case when ${schedulingHost.nextFeeAt} <= transaction_timestamp() + then floor(extract(epoch from transaction_timestamp() - ${schedulingHost.nextFeeAt}) * 1000 + / ${HOST_FEE_INTERVAL_MS})::int + 1 + else 0 end`, + }); + const dueDays = deleted?.dueDays ?? 0; + if (dueDays > 0) { + if (opts.billing) { + await countTowardAllowance(tx, opts.billing, { + organizationId: host.organizationId, + meter: HOST_FEE_METER, + days: dueDays, + }); + } + await recordUsage(tx, { + organizationId: host.organizationId, + environmentId: host.environmentId, + projectId: host.projectId, + meter: HOST_FEE_METER, + quantity: dueDays, + }); + } return { id: host.id }; }); } +/** + * System-level: charges every live host's due day (`scheduling.hosts`), when the day starts. Each + * day counts toward a capped plan's allowance while it lasts; days past it are still recorded (and + * written off). Returns how many host-days were charged. + */ +export async function chargeDueHostFees( + opts: { + now?: Date; + limit?: number; + scope?: { organizationIds: string[] }; + billing?: UsageGate | undefined; + } = {}, +): Promise { + const now = opts.now ?? new Date(); + const limit = opts.limit ?? 1000; + return db.transaction(async (tx) => { + const due = tx + .select({ id: schedulingHost.id }) + .from(schedulingHost) + .where( + and( + lte(schedulingHost.nextFeeAt, now), + opts.scope + ? inArray(schedulingHost.organizationId, opts.scope.organizationIds) + : undefined, + ), + ) + .orderBy(asc(schedulingHost.nextFeeAt)) + .limit(limit) + .for("update", { skipLocked: true }); + const charged = await tx + .update(schedulingHost) + .set({ + nextFeeAt: sql`${schedulingHost.nextFeeAt} + ${`${HOST_FEE_INTERVAL_MS} milliseconds`}::interval`, + }) + .where(and(inArray(schedulingHost.id, due), lte(schedulingHost.nextFeeAt, now))) + .returning({ + organizationId: schedulingHost.organizationId, + projectId: schedulingHost.projectId, + environmentId: schedulingHost.environmentId, + }); + const byEnvironment = new Map(); + for (const row of charged) { + const entry = byEnvironment.get(row.environmentId) ?? { source: row, days: 0 }; + entry.days++; + byEnvironment.set(row.environmentId, entry); + } + for (const { source, days } of byEnvironment.values()) { + if (opts.billing) { + await countTowardAllowance(tx, opts.billing, { + organizationId: source.organizationId, + meter: HOST_FEE_METER, + days, + }); + } + await recordUsage(tx, { ...source, meter: HOST_FEE_METER, quantity: days }); + } + return charged.length; + }); +} + /** Active hosts matching every attribute: a list attribute matches if it contains the value. */ export function attributesMatch( hostAttributes: Record, diff --git a/packages/scheduling/src/index.ts b/packages/scheduling/src/index.ts index 588c163d2..941aa4c31 100644 --- a/packages/scheduling/src/index.ts +++ b/packages/scheduling/src/index.ts @@ -15,6 +15,7 @@ export * from "./event-types.ts"; export * from "./hosts.ts"; export * from "./logo.ts"; export * from "./notifications.ts"; +export * from "./plan.ts"; export * from "./reencrypt.ts"; export * from "./retention.ts"; export * from "./schedules.ts"; diff --git a/packages/scheduling/src/plan.ts b/packages/scheduling/src/plan.ts new file mode 100644 index 000000000..fe5877c9d --- /dev/null +++ b/packages/scheduling/src/plan.ts @@ -0,0 +1,51 @@ +/** + * What the plan and the account's billing decide about Scheduling. Live environments only: sandbox + * has everything, so a team builds first and meets the plan at go-live. These refuse new setup and + * new bookings; what already exists (a filter, a confirmed booking, a hold being confirmed) keeps + * working whatever the plan says. + */ +import type { FeatureKey, PlanGate } from "@coss/billing/gate"; +import { BUDGET_EXCEEDED_MESSAGES, budgetHoldFor, isOnBillingHold } from "@coss/billing/metering"; +import { SchedulingError } from "./errors.ts"; +import type { SchedulingEnvironment } from "./types.ts"; + +type Environment = Pick & { + mode?: SchedulingEnvironment["mode"]; +}; + +/** Throws a `BillingError` when a live environment's plan lacks the feature. */ +export async function requireLiveFeature( + env: Environment, + plans: PlanGate | undefined, + feature: FeatureKey, +): Promise { + if (env.mode === "live") await plans?.requireFeature(env.organizationId, feature); +} + +/** Asking for hosts by attributes (`host_filter`, `attributes`). */ +export async function requireRouting( + env: Environment, + plans: PlanGate | undefined, + attributes: Readonly> | null | undefined, +): Promise { + if (attributes && Object.keys(attributes).length > 0) { + await requireLiveFeature(env, plans, "schedulingRouting"); + } +} + +/** A live new booking is usage: an overdue invoice, spend limit or project budget stops it. */ +export async function refuseNewBookingWhileHeld(env: SchedulingEnvironment): Promise { + if (env.mode !== "live") return; + if (await isOnBillingHold(env.organizationId)) { + throw new SchedulingError( + "billing_hold", + "New live bookings are paused: your organization has an overdue invoice or reached its " + + "spend limit. Sandbox keys still work.", + ); + } + const held = await budgetHoldFor({ + organizationId: env.organizationId, + projectId: env.projectId, + }); + if (held) throw new SchedulingError("budget_exceeded", BUDGET_EXCEEDED_MESSAGES[held]); +} diff --git a/packages/scheduling/src/sessions.ts b/packages/scheduling/src/sessions.ts index 0c1bb5ddb..695cd40e0 100644 --- a/packages/scheduling/src/sessions.ts +++ b/packages/scheduling/src/sessions.ts @@ -6,6 +6,7 @@ * can do, expires, and stops working when the booking is cancelled. */ import { createHash, randomBytes } from "node:crypto"; +import type { PlanGate } from "@coss/billing/gate"; import { db, schema } from "@coss/db"; import { ID_PREFIXES, isId, newId } from "@coss/db/id"; import { withoutTrailingSlashes } from "@coss/db/url"; @@ -16,6 +17,7 @@ import type { BookingActor } from "./bookings.ts"; import { SchedulingError } from "./errors.ts"; import { resolveEventType } from "./event-types.ts"; import { resolveHost } from "./hosts.ts"; +import { requireLiveFeature } from "./plan.ts"; import type { AttendeeInputOutput } from "./schemas.ts"; import type { SchedulingDeps, SchedulingEnvironment } from "./types.ts"; @@ -116,8 +118,11 @@ export async function createHostSession( createdByUserId?: string | null; publicUrl: string | null; now?: Date; + /** The API's gate for embedding the portal. A dashboard member's own look passes none. */ + plans?: PlanGate | undefined; }, ): Promise { + await requireLiveFeature(env, input.plans, "schedulingHostPortal"); const host = await resolveHost(db, env, input.host); return mint({ env, diff --git a/packages/scheduling/src/types.ts b/packages/scheduling/src/types.ts index 623118edd..55a9c4932 100644 --- a/packages/scheduling/src/types.ts +++ b/packages/scheduling/src/types.ts @@ -1,4 +1,4 @@ -import type { UsageGate } from "@coss/billing/gate"; +import type { PlanGate, UsageGate } from "@coss/billing/gate"; import type { SecretsKeyRing } from "@coss/webhooks"; import type { CalendarDeps } from "./calendar/connections.ts"; import type { SetupScreening } from "./screening.ts"; @@ -18,6 +18,8 @@ export type SchedulingDeps = { lookupKey: Buffer; /** Reserves capped plans' bookings (live only). */ billing?: UsageGate | undefined; + /** What the plan includes and allows (live only). */ + plans?: PlanGate | undefined; /** Calendars: a last look at the provider before booking. Without it, synced data alone. */ calendars?: CalendarDeps | undefined; /** `SCHEDULING_PUBLIC_URL`: the hosted pages sessions link to. */ diff --git a/packages/sdk/src/types/billing.ts b/packages/sdk/src/types/billing.ts index acfd7fc83..345830ccb 100644 --- a/packages/sdk/src/types/billing.ts +++ b/packages/sdk/src/types/billing.ts @@ -20,8 +20,8 @@ export type Meter = | "verify.sms" | "forms.submissions" | "forms.file_uploads" - | "video.participant_minutes" | "scheduling.bookings" + | "scheduling.hosts" | "hooks.messages"; export type BillingHoldReason = "delinquent" | "spend_limit" | "admin"; diff --git a/packages/sign/src/errors.ts b/packages/sign/src/errors.ts index 3d1052029..1af5e090e 100644 --- a/packages/sign/src/errors.ts +++ b/packages/sign/src/errors.ts @@ -10,6 +10,8 @@ export type SignErrorCode = | "request_not_draft" | "request_not_cancelable" | "account_suspended" + | "billing_hold" + | "budget_exceeded" | "invalid_origin" | "request_not_signable" | "signer_not_embedded" @@ -39,6 +41,8 @@ const STATUS_BY_CODE = { request_not_draft: 422, request_not_cancelable: 422, account_suspended: 403, + billing_hold: 402, + budget_exceeded: 402, // An origin allowed to embed the signing page that isn't `https://host[:port]`. invalid_origin: 422, // Signing needs a `sent` request that hasn't expired. diff --git a/packages/sign/src/requests.ts b/packages/sign/src/requests.ts index e52ca00e9..24a911050 100644 --- a/packages/sign/src/requests.ts +++ b/packages/sign/src/requests.ts @@ -3,6 +3,7 @@ * with their signers and fields, in one transaction with the first events of their audit trail. * Reads return the latest shape (`SignRequestObject`). */ +import { BUDGET_EXCEEDED_MESSAGES, budgetHoldFor, isOnBillingHold } from "@coss/billing/metering"; import { db, schema, type Transaction } from "@coss/db"; import { ID_PREFIXES, isId, newId } from "@coss/db/id"; import { @@ -395,10 +396,36 @@ async function transition( return getSignRequest(input); } +/** A live request is usage once sealed, and its emails cost before that. */ +async function refuseWhileHeld(source: { + organizationId: string; + projectId: string; +}): Promise { + if (await isOnBillingHold(source.organizationId)) { + throw new SignError( + "billing_hold", + "New live signature requests are paused: your organization has an overdue invoice or " + + "reached its spend limit. Sandbox keys still work.", + ); + } + const held = await budgetHoldFor(source); + if (held) throw new SignError("budget_exceeded", BUDGET_EXCEEDED_MESSAGES[held]); +} + export async function sendSignRequest( input: { organizationId: string; environmentId: string; requestId: string; actor: SignActor }, now: Date = new Date(), ): Promise { + if (!(await environmentIsSandbox(input))) { + const [draft] = await db + .select({ projectId: signRequest.projectId }) + .from(signRequest) + .where(requestScope(input)) + .limit(1); + if (draft) { + await refuseWhileHeld({ organizationId: input.organizationId, projectId: draft.projectId }); + } + } return transition( input, { @@ -480,6 +507,9 @@ export function createSignService(opts: SignServiceOptions): SignService { ): Promise<{ request: SignRequestObjectOutput; replayed: boolean }> { const { environment: env, params } = input; const now = clock(); + if (env.mode === "live") { + await refuseWhileHeld({ organizationId: env.organizationId, projectId: env.projectId }); + } // `expires_at` is checked against the clock only for a new request (inside `insert`): a replay // of the same key and body returns the original even once that time has passed. const expiresAt = params.expires_at ? new Date(params.expires_at) : null; diff --git a/packages/sms/test/notifications.test.ts b/packages/sms/test/notifications.test.ts index be0b40fcd..670651d6d 100644 --- a/packages/sms/test/notifications.test.ts +++ b/packages/sms/test/notifications.test.ts @@ -20,6 +20,9 @@ const DASHBOARD = "https://coss.test"; const anyPlan: PlanGate = { async requireFeature() {}, + async hasFeature() { + return true; + }, async limit() { return null; }, diff --git a/packages/sms/test/provisioning.test.ts b/packages/sms/test/provisioning.test.ts index c914cd3d2..0305368cf 100644 --- a/packages/sms/test/provisioning.test.ts +++ b/packages/sms/test/provisioning.test.ts @@ -38,6 +38,9 @@ function testTollFreeNumber(): string { function plansAllowing(numbers: number | null): PlanGate { return { async requireFeature() {}, + async hasFeature() { + return true; + }, async limit() { return numbers; }, @@ -353,6 +356,9 @@ test("plans cap numbers; released numbers stop sending; deletion closes the suba ...deps(), plans: { async requireFeature() {}, + async hasFeature() { + return true; + }, async limit() { return 1; }, From e9c40f633b199a51d7725df6435dc9144cb6db0d Mon Sep 17 00:00:00 2001 From: Bailey Pumfleet Date: Wed, 4 Nov 2026 15:03:21 -0600 Subject: [PATCH 2/9] Keep the booking page's pricing to the four cells it has room for The merged pricing tray fits at most four: SMS reminders stay in the text. --- apps/site/content/solutions/booking.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/site/content/solutions/booking.mdx b/apps/site/content/solutions/booking.mdx index 90401409a..ff07f966e 100644 --- a/apps/site/content/solutions/booking.mdx +++ b/apps/site/content/solutions/booking.mdx @@ -220,7 +220,7 @@ Bookings and calendars send [webhooks](/docs/webhooks) with ids, times and statu
- + The Free plan includes and hosts, and Scale includes . A marketplace's search, attribute routing and embedded host portal need . Booking emails sent from your own domain are billed like any email and SMS reminders like any text message, and live SMS needs a paid plan. Sandbox environments are free. See [pricing](/pricing) for the plans. From c387cbd658a11b7bddb9d75573c2621dd766f418 Mon Sep 17 00:00:00 2001 From: Bailey Pumfleet Date: Wed, 4 Nov 2026 15:10:55 -0600 Subject: [PATCH 3/9] Address Codex review (round 1) - A new host past a capped plan's allowance is refused, like a new inbox, so deleting and creating can't run past it. - The holds route checks attribute routing, like bookings and series. - A retry of a booking, series or signature request already made still answers with it during a billing hold, and sending a request already sent returns it. - The footer reads the plan in effect, not what a scheduled downgrade allows, and never shows in a sandbox. --- apps/api/src/routes/scheduling-client.ts | 9 +- apps/api/src/routes/scheduling.ts | 1 + apps/api/test/scheduling-plans.test.ts | 140 +++++++++++++++++- apps/site/content/docs/(platform)/billing.mdx | 2 +- .../content/docs/scheduling/embedding.mdx | 2 +- packages/billing/src/enforcement.ts | 5 +- packages/billing/src/gate.ts | 2 +- packages/scheduling/AGENTS.md | 8 +- packages/scheduling/src/bookings.ts | 4 +- packages/scheduling/src/hosts.ts | 14 +- packages/scheduling/src/plan.ts | 21 ++- packages/sign/src/requests.ts | 33 +++-- 12 files changed, 194 insertions(+), 47 deletions(-) diff --git a/apps/api/src/routes/scheduling-client.ts b/apps/api/src/routes/scheduling-client.ts index c9ac55119..7c8d220b7 100644 --- a/apps/api/src/routes/scheduling-client.ts +++ b/apps/api/src/routes/scheduling-client.ts @@ -248,10 +248,11 @@ export function createSchedulingClientRoutes(opts: { brand_name: settings?.brandName ?? null, accent_color: settings?.accentColor ?? null, logo: logo ? { data_url: logoDataUrl(logo), width: logo.width, height: logo.height } : null, - // "Scheduling by coss.com" on the page, on plans without the white label. - coss_attribution: deps.plans - ? !(await deps.plans.hasFeature(env.organizationId, "schedulingWhiteLabel")) - : false, + // "Scheduling by coss.com" on a live page, on plans without the white label. + coss_attribution: + env.mode === "live" && deps.plans + ? !(await deps.plans.hasFeature(env.organizationId, "schedulingWhiteLabel")) + : false, }; }; diff --git a/apps/api/src/routes/scheduling.ts b/apps/api/src/routes/scheduling.ts index 39ac3db77..afd03846a 100644 --- a/apps/api/src/routes/scheduling.ts +++ b/apps/api/src/routes/scheduling.ts @@ -1536,6 +1536,7 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA router.openapi( createHoldRoute, async (c) => { + await requireRouting(c.var.auth, deps.plans, c.req.valid("json").attributes); const { booking, replayed } = await createHold( c.var.auth, bookingRequest(c.req.valid("json"), c.req.valid("header")["Idempotency-Key"]), diff --git a/apps/api/test/scheduling-plans.test.ts b/apps/api/test/scheduling-plans.test.ts index d856f67fa..c4e4beb96 100644 --- a/apps/api/test/scheduling-plans.test.ts +++ b/apps/api/test/scheduling-plans.test.ts @@ -1,12 +1,14 @@ import { describe, expect, test } from "bun:test"; -import { createPlanGate, createUsageGate, placeHold } from "@coss/billing"; +import { BillingError, createPlanGate, createUsageGate, placeHold } from "@coss/billing"; +import type { UsageGate } from "@coss/billing/gate"; import { createTestContract } from "@coss/billing/testing"; import { db, pool } from "@coss/db"; import { createForm, type FormEnvironment } from "@coss/forms"; import { CreateFormRequest } from "@coss/forms/schemas"; import { testFormChecks } from "@coss/forms/testing"; import { memoryLimiter } from "@coss/ratelimit/testing"; -import { chargeDueHostFees } from "@coss/scheduling"; +import { chargeDueHostFees, countLiveHosts, createHost } from "@coss/scheduling"; +import { buildTestPdfBase64 } from "@coss/sign/testing"; import { createTestTenant, type TestTenant } from "@coss/tenancy/testing"; import { apiRequest, createTestApp, errorOf, type TestApp } from "./helpers.ts"; @@ -145,6 +147,14 @@ describe("Scheduling's plan gates", () => { const pro = await onPlan("pro"); const app = enforced(); const eventType = await bookable(app, pro.liveKey); + const onHold = await call(app, pro.liveKey, "POST", "/holds", { + event_type: eventType, + start: at("09:00"), + attendee, + attributes: { specialty: "dermatology" }, + }); + expect(onHold.status).toBe(402); + expect((await errorOf(onHold)).code).toBe("plan_feature_unavailable"); const filtered = await call(app, pro.liveKey, "POST", "/event_types", { title: "Pool", kind: "pool", @@ -249,9 +259,9 @@ describe("Scheduling's plan gates", () => { test("pages show the attribution below Scale, and not on it", async () => { const app = enforced(); - const sessionOf = async (tenant: TestTenant) => { - const eventType = await bookable(app, tenant.liveKey); - const res = await call(app, tenant.liveKey, "POST", "/booker_sessions", { + const sessionOf = async (tenant: TestTenant, key = tenant.liveKey) => { + const eventType = await bookable(app, key); + const res = await call(app, key, "POST", "/booker_sessions", { event_type: eventType, }); const { token } = (await res.json()) as { token: string }; @@ -262,10 +272,57 @@ describe("Scheduling's plan gates", () => { }; expect((await sessionOf(await createTestTenant())).coss_attribution).toBe(true); expect((await sessionOf(await onPlan("scale"))).coss_attribution).toBe(false); + // A sandbox has everything on every plan. + const sandbox = await createTestTenant(); + expect((await sessionOf(sandbox, sandbox.sandboxKey)).coss_attribution).toBe(false); + }); + + test("a scheduled downgrade keeps the attribution off until it applies", async () => { + const tenant = await createTestTenant(); + await createTestContract( + tenant.organization.id, + { version: 1, plan: "scale" }, + { + effectiveFrom: new Date("2026-01-01T00:00:00Z"), + effectiveUntil: new Date("2099-01-01T00:00:00Z"), + }, + ); + await createTestContract( + tenant.organization.id, + { version: 1, plan: "pro" }, + { effectiveFrom: new Date("2099-01-01T00:00:00Z") }, + ); + expect(await createPlanGate().hasFeature(tenant.organization.id, "schedulingWhiteLabel")).toBe( + true, + ); }); }); describe("the host fee", () => { + test("past a capped plan's allowance a new host is refused, an existing one written off", async () => { + const tenant = await createTestTenant(); + const refusing: UsageGate = { + async reserve() { + throw new BillingError("usage_limit_exceeded", "Past the allowance."); + }, + }; + const env = { + organizationId: tenant.organization.id, + projectId: tenant.project.id, + environmentId: tenant.production.id, + mode: "live" as const, + }; + const after = new Date("2027-02-01T00:00:00Z"); + await expect( + createHost( + env, + { uid: "over", name: "Over", time_zone: "UTC" }, + { billing: refusing, now: after }, + ), + ).rejects.toMatchObject({ code: "usage_limit_exceeded" }); + expect(await countLiveHosts(tenant.organization.id)).toBe(0); + }); + const usage = async (organizationId: string) => { const { rows } = await pool.query<{ quantity: string }>( "select coalesce(sum(quantity), 0) as quantity from billing_usage where organization_id = $1 and meter = 'scheduling.hosts'", @@ -327,6 +384,43 @@ describe("holds reach every new live usage", () => { expect((await call(app, tenant.liveKey, "POST", `/holds/${id}/confirm`, {})).status).toBe(200); }); + test("a retry of what was already made still answers with it", async () => { + const tenant = await held(); + const app = enforced(); + const eventType = await bookable(app, tenant.liveKey); + const keyed = (path: string, body: object, key: string) => + app.request(`/v1/scheduling${path}`, { + method: "POST", + headers: { + Authorization: `Bearer ${tenant.liveKey}`, + "Content-Type": "application/json", + "Idempotency-Key": key, + }, + body: JSON.stringify(body), + }); + const booking = { event_type: eventType, start: at("09:00"), attendee }; + const series = { + event_type: eventType, + start: at("12:00"), + attendee, + recurrence: { frequency: "weekly", interval: 1, count: 2 }, + }; + const made = await keyed("/bookings", booking, "booking-key"); + expect(made.status).toBe(201); + const madeSeries = await keyed("/series", series, "series-key"); + expect(madeSeries.status).toBe(201); + await hold(tenant); + const again = await keyed("/bookings", booking, "booking-key"); + expect(again.status).toBe(201); + expect(again.headers.get("Idempotent-Replayed")).toBe("true"); + const seriesAgain = await keyed("/series", series, "series-key"); + expect(seriesAgain.status).toBe(201); + expect(seriesAgain.headers.get("Idempotent-Replayed")).toBe("true"); + expect((await keyed("/bookings", { ...booking, start: at("10:00") }, "new-key")).status).toBe( + 402, + ); + }); + test("the person on the booking page doesn't hear about the customer's billing", async () => { const tenant = await held(); const app = enforced(); @@ -407,6 +501,40 @@ describe("holds reach every new live usage", () => { expect((await errorOf(refused)).code).toBe("billing_hold"); }); + test("a retry of a signature request, and sending one already sent, still answer", async () => { + const tenant = await held(); + const { app } = createTestApp(); + const body = { + title: "NDA", + documents: [{ filename: "nda.pdf", content: await buildTestPdfBase64() }], + signers: [ + { + name: "Ada Lovelace", + email: "ada@example.com", + fields: [{ type: "signature", anchor: "{{signature:signer_1}}" }], + }, + ], + }; + const send = () => + apiRequest(app, "POST", "/v1/sign/requests", { + key: tenant.liveKey, + headers: { "Idempotency-Key": "sign-key" }, + body, + }); + const made = await send(); + expect(made.status).toBe(201); + const { id } = (await made.json()) as { id: string }; + await hold(tenant); + const again = await send(); + expect(again.status).toBeLessThan(300); + expect(((await again.json()) as { id: string }).id).toBe(id); + const sentAgain = await apiRequest(app, "POST", `/v1/sign/requests/${id}/send`, { + key: tenant.liveKey, + body: {}, + }); + expect(sentAgain.status).toBe(200); + }); + test("new live signature requests are refused", async () => { const tenant = await held(); const { app } = createTestApp(); @@ -415,7 +543,7 @@ describe("holds reach every new live usage", () => { key: tenant.liveKey, body: { title: "NDA", - documents: [{ filename: "nda.pdf", content: Buffer.from("%PDF").toString("base64") }], + documents: [{ filename: "nda.pdf", content: await buildTestPdfBase64() }], signers: [ { name: "Ada Lovelace", diff --git a/apps/site/content/docs/(platform)/billing.mdx b/apps/site/content/docs/(platform)/billing.mdx index 4a711f0ef..159d64ae8 100644 --- a/apps/site/content/docs/(platform)/billing.mdx +++ b/apps/site/content/docs/(platform)/billing.mdx @@ -69,7 +69,7 @@ after removing it. On a contract, add-ons are part of the contract. - On **Free**, live sends past the month's allowance are refused with a `402` `billing_error` (`usage_limit_exceeded`) until the next month or an upgrade, and so are new live inboxes past - its inbox-days. Received mail past its allowance is dropped (and counted on **Received emails**). + its inbox-days and new live Scheduling hosts past its host-days. Received mail past its allowance is dropped (and counted on **Received emails**). - If an invoice can't be collected, we retry it and email your billing contacts. Once it's overdue past the grace period (14 days unless your contract says otherwise), live sending is paused (`402`, `billing_hold`) until it's paid. So are new live bookings, signature requests and diff --git a/apps/site/content/docs/scheduling/embedding.mdx b/apps/site/content/docs/scheduling/embedding.mdx index 7fac0e998..3d78d1f03 100644 --- a/apps/site/content/docs/scheduling/embedding.mdx +++ b/apps/site/content/docs/scheduling/embedding.mdx @@ -13,7 +13,7 @@ thing, with its token in the URL's fragment, which never reaches a server. | `createForBooking` | Reschedule or cancel one booking | Until a day after it ends. Stops when it's cancelled | In live environments, `createForHost` needs . On other plans, connect -a host's calendars and set their hours through the API. Pages show "Scheduling by coss.com" below them, except on +a host's calendars and set their hours through the API. Live pages show "Scheduling by coss.com" below them, except on . ```ts twoslash diff --git a/packages/billing/src/enforcement.ts b/packages/billing/src/enforcement.ts index cc0dca7ea..a209de22d 100644 --- a/packages/billing/src/enforcement.ts +++ b/packages/billing/src/enforcement.ts @@ -83,7 +83,10 @@ export function createPlanGate(opts: { systemOrganizationId?: string | null } = }, async hasFeature(organizationId, feature) { if (exempt(organizationId)) return true; - return (await entitlementsFor(organizationId)).features[feature]; + // The plan in effect: a scheduled downgrade only narrows what can be created. + return (await billingStateAt(organizationId, new Date())).terms.entitlements.features[ + feature + ]; }, async requireCapacity(organizationId, limit, current, count = 1) { if (exempt(organizationId)) return; diff --git a/packages/billing/src/gate.ts b/packages/billing/src/gate.ts index b33414562..5fdce9c4a 100644 --- a/packages/billing/src/gate.ts +++ b/packages/billing/src/gate.ts @@ -12,7 +12,7 @@ export type { FeatureKey, LimitKey, MeterKey }; export interface PlanGate { /** Throws a `BillingError` (`plan_feature_unavailable`) when the plan lacks the feature. */ requireFeature(organizationId: string, feature: FeatureKey): Promise; - /** Whether the plan has the feature, for what changes rather than refuses. */ + /** Whether the plan in effect has the feature, for what changes rather than refuses. */ hasFeature(organizationId: string, feature: FeatureKey): Promise; /** * Throws a `BillingError` (`plan_limit_reached`) when `count` more would exceed the plan's diff --git a/packages/scheduling/AGENTS.md b/packages/scheduling/AGENTS.md index 4038e8fd7..4a79a8c3a 100644 --- a/packages/scheduling/AGENTS.md +++ b/packages/scheduling/AGENTS.md @@ -195,8 +195,9 @@ on `usage_recorded_at` (`recordUsage` doesn't dedupe). `reserve` runs after a ho aren't charged; reschedules are free; cancelled stays charged. `scheduling.hosts` is one unit per day a live host exists (`next_fee_at`, its presence is what makes a host live for -the plan's count): on creation, then `chargeDueHostFees` (the worker's `scheduling-host-fees`), and `deleteHost` charges -what's due. Nothing is recorded before `meterPricedFrom` (2027-01-01): hosts wait with `next_fee_at` at that date. +the plan's count): on creation (reserved like a new inbox: past a capped plan's allowance it's refused), then +`chargeDueHostFees` (the worker's `scheduling-host-fees`), and `deleteHost` charges what's due (those two only ever write +off past the allowance). Nothing is recorded before `meterPricedFrom` (2027-01-01): hosts wait with `next_fee_at` at that date. ## Plans and holds @@ -217,7 +218,8 @@ A person on a hosted page never meets a plan gate (Free's booking allowance asid checked again. Never gate the double-booking guarantee, calendars or series. A filter or a group size that exists keeps working whatever the plan; a downgrade is refused only for live `host_filter`s (the dashboard's preflight). -`refuseNewBookingWhileHeld`: a billing hold or the project's budget stops new live bookings, holds and series (402), and +`refuseNewBookingWhileHeld`, once the row is known to be new so a retry still answers with what it made: a billing hold +or the project's budget stops new live bookings, holds and series (402), and the booker's page says only that the account can't take bookings. Confirming a hold and approving keep working: the customer may have taken payment for them. diff --git a/packages/scheduling/src/bookings.ts b/packages/scheduling/src/bookings.ts index bcb43e10f..8eb57a863 100644 --- a/packages/scheduling/src/bookings.ts +++ b/packages/scheduling/src/bookings.ts @@ -1426,7 +1426,6 @@ async function book( actor: BookingActor, wantHold: boolean, ): Promise<{ booking: BookingObjectOutput; replayed: boolean }> { - await refuseNewBookingWhileHeld(env); checkStart(request.startMs); checkAttendee(request.attendee); const eventType = await resolveEventType(db, env, request.eventType); @@ -1469,6 +1468,7 @@ async function book( idempotencyKey: key, }); if (!inserted) return undefined; + await refuseNewBookingWhileHeld(env, tx); return createOne(tx, context, request, inserted, checked, plan); })(); @@ -2711,7 +2711,6 @@ export async function createSeries( deps: SchedulingDeps, actor: BookingActor, ): Promise<{ result: SeriesResult; replayed: boolean }> { - await refuseNewBookingWhileHeld(env); checkStart(request.startMs); checkAttendee(request.attendee); const eventType = await resolveEventType(db, env, request.eventType); @@ -2815,6 +2814,7 @@ export async function createSeries( }) .returning(); if (!series) return undefined; + await refuseNewBookingWhileHeld(env, tx); let best: { chosen: Candidates; conflicts: number } | null = null; for (const chosen of choiceSets) { try { diff --git a/packages/scheduling/src/hosts.ts b/packages/scheduling/src/hosts.ts index fa6c19b40..c95bc5674 100644 --- a/packages/scheduling/src/hosts.ts +++ b/packages/scheduling/src/hosts.ts @@ -187,13 +187,13 @@ export async function createHost( }) .returning(); if (chargedNow) { - if (opts.billing) { - await countTowardAllowance(tx, opts.billing, { - organizationId: env.organizationId, - meter: HOST_FEE_METER, - days: 1, - }); - } + // Past a capped plan's allowance a new host is refused, or deleting and creating would + // never run out. Hosts that already exist are only ever written off. + await opts.billing?.reserve(tx, { + organizationId: env.organizationId, + meter: HOST_FEE_METER, + quantity: 1, + }); await recordUsage(tx, { organizationId: env.organizationId, environmentId: env.environmentId, diff --git a/packages/scheduling/src/plan.ts b/packages/scheduling/src/plan.ts index fe5877c9d..8c46b79ac 100644 --- a/packages/scheduling/src/plan.ts +++ b/packages/scheduling/src/plan.ts @@ -6,6 +6,7 @@ */ import type { FeatureKey, PlanGate } from "@coss/billing/gate"; import { BUDGET_EXCEEDED_MESSAGES, budgetHoldFor, isOnBillingHold } from "@coss/billing/metering"; +import type { Executor } from "@coss/db"; import { SchedulingError } from "./errors.ts"; import type { SchedulingEnvironment } from "./types.ts"; @@ -33,19 +34,25 @@ export async function requireRouting( } } -/** A live new booking is usage: an overdue invoice, spend limit or project budget stops it. */ -export async function refuseNewBookingWhileHeld(env: SchedulingEnvironment): Promise { +/** + * A live new booking is usage: an overdue invoice, spend limit or project budget stops it. Called + * once the row is known to be new, so a retry of a booking already made still answers with it. + */ +export async function refuseNewBookingWhileHeld( + env: SchedulingEnvironment, + executor: Executor, +): Promise { if (env.mode !== "live") return; - if (await isOnBillingHold(env.organizationId)) { + if (await isOnBillingHold(env.organizationId, executor)) { throw new SchedulingError( "billing_hold", "New live bookings are paused: your organization has an overdue invoice or reached its " + "spend limit. Sandbox keys still work.", ); } - const held = await budgetHoldFor({ - organizationId: env.organizationId, - projectId: env.projectId, - }); + const held = await budgetHoldFor( + { organizationId: env.organizationId, projectId: env.projectId }, + executor, + ); if (held) throw new SchedulingError("budget_exceeded", BUDGET_EXCEEDED_MESSAGES[held]); } diff --git a/packages/sign/src/requests.ts b/packages/sign/src/requests.ts index 24a911050..3040f5aaf 100644 --- a/packages/sign/src/requests.ts +++ b/packages/sign/src/requests.ts @@ -4,7 +4,7 @@ * Reads return the latest shape (`SignRequestObject`). */ import { BUDGET_EXCEEDED_MESSAGES, budgetHoldFor, isOnBillingHold } from "@coss/billing/metering"; -import { db, schema, type Transaction } from "@coss/db"; +import { db, type Executor, schema, type Transaction } from "@coss/db"; import { ID_PREFIXES, isId, newId } from "@coss/db/id"; import { requestHash as hashRequest, @@ -397,18 +397,18 @@ async function transition( } /** A live request is usage once sealed, and its emails cost before that. */ -async function refuseWhileHeld(source: { - organizationId: string; - projectId: string; -}): Promise { - if (await isOnBillingHold(source.organizationId)) { +async function refuseWhileHeld( + source: { organizationId: string; projectId: string }, + executor: Executor = db, +): Promise { + if (await isOnBillingHold(source.organizationId, executor)) { throw new SignError( "billing_hold", "New live signature requests are paused: your organization has an overdue invoice or " + "reached its spend limit. Sandbox keys still work.", ); } - const held = await budgetHoldFor(source); + const held = await budgetHoldFor(source, executor); if (held) throw new SignError("budget_exceeded", BUDGET_EXCEEDED_MESSAGES[held]); } @@ -417,13 +417,14 @@ export async function sendSignRequest( now: Date = new Date(), ): Promise { if (!(await environmentIsSandbox(input))) { - const [draft] = await db - .select({ projectId: signRequest.projectId }) + const [found] = await db + .select({ projectId: signRequest.projectId, status: signRequest.status }) .from(signRequest) .where(requestScope(input)) .limit(1); - if (draft) { - await refuseWhileHeld({ organizationId: input.organizationId, projectId: draft.projectId }); + // A request already sent is returned as it is. + if (found?.status === "draft") { + await refuseWhileHeld({ organizationId: input.organizationId, projectId: found.projectId }); } } return transition( @@ -507,9 +508,6 @@ export function createSignService(opts: SignServiceOptions): SignService { ): Promise<{ request: SignRequestObjectOutput; replayed: boolean }> { const { environment: env, params } = input; const now = clock(); - if (env.mode === "live") { - await refuseWhileHeld({ organizationId: env.organizationId, projectId: env.projectId }); - } // `expires_at` is checked against the clock only for a new request (inside `insert`): a replay // of the same key and body returns the original even once that time has passed. const expiresAt = params.expires_at ? new Date(params.expires_at) : null; @@ -609,6 +607,13 @@ export function createSignService(opts: SignServiceOptions): SignService { .returning(); // A replay (the key's row exists) returns before this; a new request rolls back here. if (!row) return undefined; + // Only a new request: a replay of one already made (no row here) still answers with it. + if (env.mode === "live") { + await refuseWhileHeld( + { organizationId: env.organizationId, projectId: env.projectId }, + tx, + ); + } checkExpiry(params.expires_at, now); if (inline.length > 0) await tx.insert(signDocument).values(inline); await tx.insert(signRequestDocument).values( From 4f0c157ad0e318f5ca534ffe9543cb2aa51702de Mon Sep 17 00:00:00 2001 From: Bailey Pumfleet Date: Wed, 4 Nov 2026 15:19:48 -0600 Subject: [PATCH 4/9] Address Codex review (round 2) - Plan checks never run inside a transaction: creating a host held an advisory lock while the gate waited for a second connection, which could stall the pool under concurrent creates. Updating an event type had the same shape. - A retry of a key already used meets no plan gate, for holds and API attributes. - An empty host_filter needs routing too: it matches every active host. - The host fee test no longer depends on today being before January 2027. --- apps/api/src/routes/scheduling.ts | 4 +- apps/api/test/scheduling-plans.test.ts | 80 ++++++++++++++++- packages/scheduling/AGENTS.md | 3 + packages/scheduling/src/bookings.ts | 40 +++++++-- packages/scheduling/src/event-types.ts | 24 +++-- packages/scheduling/src/hosts.ts | 120 ++++++++++++++----------- 6 files changed, 199 insertions(+), 72 deletions(-) diff --git a/apps/api/src/routes/scheduling.ts b/apps/api/src/routes/scheduling.ts index afd03846a..7769bca9f 100644 --- a/apps/api/src/routes/scheduling.ts +++ b/apps/api/src/routes/scheduling.ts @@ -1423,6 +1423,7 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA body: z.output, idempotencyKey: string | undefined, ) => ({ + gateRouting: true, eventType: body.event_type, startMs: instant(body.start, "start"), host: body.host, @@ -1439,7 +1440,6 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA router.openapi( createBookingRoute, async (c) => { - await requireRouting(c.var.auth, deps.plans, c.req.valid("json").attributes); const { booking, replayed } = await createBooking( c.var.auth, bookingRequest(c.req.valid("json"), c.req.valid("header")["Idempotency-Key"]), @@ -1536,7 +1536,6 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA router.openapi( createHoldRoute, async (c) => { - await requireRouting(c.var.auth, deps.plans, c.req.valid("json").attributes); const { booking, replayed } = await createHold( c.var.auth, bookingRequest(c.req.valid("json"), c.req.valid("header")["Idempotency-Key"]), @@ -1580,7 +1579,6 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA router.openapi( createSeriesRoute, async (c) => { - await requireRouting(c.var.auth, deps.plans, c.req.valid("json").attributes); const { result, replayed } = await createSeries( c.var.auth, seriesRequest(c.req.valid("json"), c.req.valid("header")["Idempotency-Key"]), diff --git a/apps/api/test/scheduling-plans.test.ts b/apps/api/test/scheduling-plans.test.ts index c4e4beb96..3f68f334a 100644 --- a/apps/api/test/scheduling-plans.test.ts +++ b/apps/api/test/scheduling-plans.test.ts @@ -102,6 +102,75 @@ describe("Scheduling's plan gates", () => { } }); + test("concurrent creates can't pass the limit, and the gate isn't asked under the lock", async () => { + const tenant = await createTestTenant(); + const gate = createPlanGate(); + let asked = 0; + const app = enforced({ + plans: { + ...gate, + requireCapacity: async (...args) => { + asked++; + return gate.requireCapacity(...args); + }, + }, + }); + const results = await Promise.all( + ["a", "b", "c", "d", "e"].map((uid) => + call(app, tenant.liveKey, "POST", "/hosts", hostBody(uid)), + ), + ); + expect(results.filter((res) => res.status === 201)).toHaveLength(3); + expect(results.filter((res) => res.status === 402)).toHaveLength(2); + expect(await countLiveHosts(tenant.organization.id)).toBe(3); + // Only the refused ones: a create that fits never takes a second connection under the lock. + expect(asked).toBe(2); + }); + + test("an empty host_filter is a filter: it matches every active host", async () => { + const pro = await onPlan("pro"); + const app = enforced(); + const res = await call(app, pro.liveKey, "POST", "/event_types", { + title: "Pool", + kind: "pool", + duration_minutes: 30, + hosts: [], + host_filter: {}, + }); + expect(res.status).toBe(402); + }); + + test("a retry of a key already used meets no plan gate", async () => { + const scale = await onPlan("scale"); + const app = enforced(); + const eventType = await bookable(app, scale.liveKey); + const body = { event_type: eventType, start: at("09:00"), attendee }; + const keyed = (target: TestApp, key: string, payload: object) => + target.request("/v1/scheduling/holds", { + method: "POST", + headers: { + Authorization: `Bearer ${scale.liveKey}`, + "Content-Type": "application/json", + "Idempotency-Key": key, + }, + body: JSON.stringify(payload), + }); + expect((await keyed(app, "hold-key", body)).status).toBe(201); + // The plan no longer has it, as after a downgrade. + const lower = enforced({ + plans: { + ...createPlanGate(), + requireFeature: async () => { + throw new BillingError("plan_feature_unavailable", "Not on this plan."); + }, + }, + }); + const replay = await keyed(lower, "hold-key", body); + expect(replay.status).toBe(201); + expect(replay.headers.get("Idempotent-Replayed")).toBe("true"); + expect((await keyed(lower, "another-key", { ...body, start: at("10:00") })).status).toBe(402); + }); + test("the embedded host portal needs Scale", async () => { const free = await createTestTenant(); const app = enforced(); @@ -334,7 +403,16 @@ describe("the host fee", () => { test("nothing is charged before the meter is priced, and a day at a time after", async () => { const tenant = await onPlan("scale"); const app = enforced(); - await call(app, tenant.liveKey, "POST", "/hosts", hostBody("a")); + await createHost( + { + organizationId: tenant.organization.id, + projectId: tenant.project.id, + environmentId: tenant.production.id, + mode: "live", + }, + hostBody("a"), + { now: new Date("2026-12-01T00:00:00Z") }, + ); await call(app, tenant.sandboxKey, "POST", "/hosts", hostBody("sandbox")); const scope = { organizationIds: [tenant.organization.id] }; expect(await chargeDueHostFees({ now: new Date("2026-12-31T23:00:00Z"), scope })).toBe(0); diff --git a/packages/scheduling/AGENTS.md b/packages/scheduling/AGENTS.md index 4a79a8c3a..fbcf0b024 100644 --- a/packages/scheduling/AGENTS.md +++ b/packages/scheduling/AGENTS.md @@ -214,6 +214,9 @@ Injected as `SchedulingDeps.plans`; without it nothing is enforced. What the pla - `schedulingWhiteLabel` (`coss_attribution` in the client API's `/session`). - `scheduling.slots.second` (`limitSchedulingSlots` in the API): live slot lists and searches per organization. +Gates take a connection of their own, so they never run inside a transaction: holding one connection while waiting for +another stalls the pool under load. They run before it, and `gateNewRequest` skips a retry of a key already used. + A person on a hosted page never meets a plan gate (Free's booking allowance aside): a session's attributes aren't checked again. Never gate the double-booking guarantee, calendars or series. A filter or a group size that exists keeps working whatever the plan; a downgrade is refused only for live `host_filter`s (the dashboard's preflight). diff --git a/packages/scheduling/src/bookings.ts b/packages/scheduling/src/bookings.ts index 8eb57a863..c6d2117c8 100644 --- a/packages/scheduling/src/bookings.ts +++ b/packages/scheduling/src/bookings.ts @@ -75,7 +75,7 @@ import { type EventTypeRow, resolveEventType } from "./event-types.ts"; import { confirmedSeats, withSessionLocation } from "./group-sessions.ts"; import { checkTimeZone, type HostRow, hostsByIds, resolveHost } from "./hosts.ts"; import { carryReminders, enqueueBookingNotifications } from "./notifications.ts"; -import { refuseNewBookingWhileHeld, requireLiveFeature } from "./plan.ts"; +import { refuseNewBookingWhileHeld, requireLiveFeature, requireRouting } from "./plan.ts"; import { type AttendeeInputOutput, type BookingObjectOutput, @@ -112,6 +112,8 @@ type Location = NonNullable; export type BookingActor = NonNullable; export type BookingRequest = { + /** The API's gate on `attributes`. A session's were checked when it was made, so it passes none. */ + gateRouting?: boolean | undefined; eventType: string; startMs: number; host?: string | undefined; @@ -1419,6 +1421,35 @@ const idempotencyErrors = { ), }; +/** + * What the plan decides about a new live request. A retry of a key already used skips it: whatever + * the plan says now, what it made is answered with (`withIdempotencyKey` checks that it matches). + */ +async function gateNewRequest( + env: SchedulingEnvironment, + request: Pick, + deps: SchedulingDeps, + opts: { hold: boolean; table: typeof schedulingBooking | typeof schedulingSeries }, +): Promise { + if (env.mode !== "live" || !deps.plans) return; + if (request.idempotencyKey) { + const [made] = await db + .select({ id: opts.table.id }) + .from(opts.table) + .where( + and( + eq(opts.table.organizationId, env.organizationId), + eq(opts.table.environmentId, env.environmentId), + eq(opts.table.idempotencyKey, request.idempotencyKey), + ), + ) + .limit(1); + if (made) return; + } + if (opts.hold) await requireLiveFeature(env, deps.plans, "schedulingHolds"); + if (request.gateRouting) await requireRouting(env, deps.plans, request.attributes); +} + async function book( env: SchedulingEnvironment, request: BookingRequest, @@ -1426,6 +1457,7 @@ async function book( actor: BookingActor, wantHold: boolean, ): Promise<{ booking: BookingObjectOutput; replayed: boolean }> { + await gateNewRequest(env, request, deps, { hold: wantHold, table: schedulingBooking }); checkStart(request.startMs); checkAttendee(request.attendee); const eventType = await resolveEventType(db, env, request.eventType); @@ -1526,10 +1558,7 @@ export const createHold = ( request: BookingRequest, deps: SchedulingDeps, actor: BookingActor, -) => - requireLiveFeature(env, deps.plans, "schedulingHolds").then(() => - book(env, request, deps, actor, true), - ); +) => book(env, request, deps, actor, true); async function lockBooking(tx: Executor, scope: Scope, id: string): Promise { if (!isId(ID_PREFIXES.schedulingBooking, id)) { @@ -2711,6 +2740,7 @@ export async function createSeries( deps: SchedulingDeps, actor: BookingActor, ): Promise<{ result: SeriesResult; replayed: boolean }> { + await gateNewRequest(env, request, deps, { hold: false, table: schedulingSeries }); checkStart(request.startMs); checkAttendee(request.attendee); const eventType = await resolveEventType(db, env, request.eventType); diff --git a/packages/scheduling/src/event-types.ts b/packages/scheduling/src/event-types.ts index 21c8e92cf..fca7604ea 100644 --- a/packages/scheduling/src/event-types.ts +++ b/packages/scheduling/src/event-types.ts @@ -20,7 +20,7 @@ import { import { notZoomMeeting } from "./calendar/zoom.ts"; import { SchedulingError } from "./errors.ts"; import { checkTimeZone, type HostRow, resolveHost } from "./hosts.ts"; -import { requireRouting } from "./plan.ts"; +import { requireLiveFeature } from "./plan.ts"; import type { CreateEventTypeRequestOutput, EventTypeObjectOutput, @@ -401,7 +401,8 @@ export async function createEventType( params: CreateEventTypeRequestOutput, opts: { screen?: SetupScreening | undefined; plans?: PlanGate | undefined } = {}, ): Promise { - await requireRouting(env, opts.plans, params.host_filter); + // `{}` is a filter too: it matches every active host. + if (params.host_filter != null) await requireLiveFeature(env, opts.plans, "schedulingRouting"); await checkGroupSeats(env, opts.plans, params.capacity); refuseLinks(params.title, "title"); refuseLinksInAddresses(params.locations); @@ -572,16 +573,21 @@ export async function updateEventType( "locations", ); } + // What it already has keeps working: only a change meets the plan. Before the transaction, since + // the gate takes a connection of its own. + if (opts.plans && (params.host_filter != null || params.capacity != null)) { + const current = await resolveEventType(db, scope, ref); + if ( + params.host_filter != null && + JSON.stringify(params.host_filter) !== JSON.stringify(current.hostFilter) + ) { + await requireLiveFeature(scope, opts.plans, "schedulingRouting"); + } + await checkGroupSeats(scope, opts.plans, params.capacity, current.capacity ?? 0); + } try { return await db.transaction(async (tx) => { const existing = await resolveEventType(tx, scope, ref); - // What it already has keeps working: only a change meets the plan. - if (JSON.stringify(params.host_filter ?? null) !== JSON.stringify(existing.hostFilter)) { - if (params.host_filter !== undefined) { - await requireRouting(scope, opts.plans, params.host_filter); - } - } - await checkGroupSeats(scope, opts.plans, params.capacity, existing.capacity ?? 0); const set = { ...(params.uid !== undefined ? { uid: params.uid } : {}), ...(params.title !== undefined ? { title: params.title } : {}), diff --git a/packages/scheduling/src/hosts.ts b/packages/scheduling/src/hosts.ts index c95bc5674..81aab6a0c 100644 --- a/packages/scheduling/src/hosts.ts +++ b/packages/scheduling/src/hosts.ts @@ -148,62 +148,74 @@ export async function createHost( const now = opts.now ?? new Date(); const pricedFrom = meterPricedFrom(HOST_FEE_METER) ?? now; const chargedNow = live && pricedFrom <= now; + // Read before the transaction: the gate takes a connection of its own, and waiting for one while + // holding this lock would stall the pool under concurrent creates. + const limit = live + ? ((await opts.plans?.limit(env.organizationId, "schedulingHosts")) ?? null) + : null; try { - return await db.transaction(async (tx) => { - if (live && opts.plans) { - // One creation at a time per organization, so the count holds. - await tx.execute( - sql`select pg_advisory_xact_lock(hashtext(${`scheduling-hosts:${env.organizationId}`}))`, - ); - const [total] = await tx - .select({ value: count() }) - .from(schedulingHost) - .where( - and( - eq(schedulingHost.organizationId, env.organizationId), - isNotNull(schedulingHost.nextFeeAt), - ), + const created = await db.transaction( + async (tx): Promise<{ host: HostObjectOutput } | { full: number }> => { + if (limit !== null) { + // One creation at a time per organization, so the count holds. + await tx.execute( + sql`select pg_advisory_xact_lock(hashtext(${`scheduling-hosts:${env.organizationId}`}))`, ); - await opts.plans.requireCapacity(env.organizationId, "schedulingHosts", total?.value ?? 0); - } - const [row] = await tx - .insert(schedulingHost) - .values({ - organizationId: env.organizationId, - projectId: env.projectId, - environmentId: env.environmentId, - uid: params.uid ?? null, - name: params.name, - email: params.email ?? null, - timeZone: params.time_zone, - attributes: params.attributes ?? {}, - locations: params.locations ?? [], - metadata: params.metadata ?? {}, - nextFeeAt: !live - ? null - : chargedNow - ? new Date(now.getTime() + HOST_FEE_INTERVAL_MS) - : pricedFrom, - }) - .returning(); - if (chargedNow) { - // Past a capped plan's allowance a new host is refused, or deleting and creating would - // never run out. Hosts that already exist are only ever written off. - await opts.billing?.reserve(tx, { - organizationId: env.organizationId, - meter: HOST_FEE_METER, - quantity: 1, - }); - await recordUsage(tx, { - organizationId: env.organizationId, - environmentId: env.environmentId, - projectId: env.projectId, - meter: HOST_FEE_METER, - quantity: 1, - }); - } - return toHostObject(row as HostRow); - }); + const [total] = await tx + .select({ value: count() }) + .from(schedulingHost) + .where( + and( + eq(schedulingHost.organizationId, env.organizationId), + isNotNull(schedulingHost.nextFeeAt), + ), + ); + const current = total?.value ?? 0; + if (current >= limit) return { full: current }; + } + const [row] = await tx + .insert(schedulingHost) + .values({ + organizationId: env.organizationId, + projectId: env.projectId, + environmentId: env.environmentId, + uid: params.uid ?? null, + name: params.name, + email: params.email ?? null, + timeZone: params.time_zone, + attributes: params.attributes ?? {}, + locations: params.locations ?? [], + metadata: params.metadata ?? {}, + nextFeeAt: !live + ? null + : chargedNow + ? new Date(now.getTime() + HOST_FEE_INTERVAL_MS) + : pricedFrom, + }) + .returning(); + if (chargedNow) { + // Past a capped plan's allowance a new host is refused, or deleting and creating would + // never run out. Hosts that already exist are only ever written off. + await opts.billing?.reserve(tx, { + organizationId: env.organizationId, + meter: HOST_FEE_METER, + quantity: 1, + }); + await recordUsage(tx, { + organizationId: env.organizationId, + environmentId: env.environmentId, + projectId: env.projectId, + meter: HOST_FEE_METER, + quantity: 1, + }); + } + return { host: toHostObject(row as HostRow) }; + }, + ); + if ("host" in created) return created.host; + // The gate's own refusal, now the lock is gone. If it no longer refuses, the plan just grew. + await opts.plans?.requireCapacity(env.organizationId, "schedulingHosts", created.full); + return await createHost(env, params, opts); } catch (error) { if (isUniqueViolation(error)) throw uidTaken(params.uid); throw error; From ee7e6995d8a26fd4e54ecb88747907636c55dc7c Mon Sep 17 00:00:00 2001 From: Bailey Pumfleet Date: Wed, 4 Nov 2026 15:25:12 -0600 Subject: [PATCH 5/9] Address Codex review (round 3) - gateRouting stays out of idempotency hashes, so retrying a request made before this deploys still replays it. - An expired idempotency key makes something new, so it meets the plan's gates. - A billing hold or project budget also stops new live hosts, each a new monthly charge. --- apps/api/test/scheduling-plans.test.ts | 67 ++++++++++++++++++- apps/site/content/docs/(platform)/billing.mdx | 4 +- apps/site/content/docs/(platform)/spend.mdx | 4 +- packages/scheduling/AGENTS.md | 5 +- packages/scheduling/src/bookings.ts | 25 +++++-- packages/scheduling/src/hosts.ts | 2 + packages/scheduling/src/plan.ts | 17 +++++ 7 files changed, 110 insertions(+), 14 deletions(-) diff --git a/apps/api/test/scheduling-plans.test.ts b/apps/api/test/scheduling-plans.test.ts index 3f68f334a..1950f86aa 100644 --- a/apps/api/test/scheduling-plans.test.ts +++ b/apps/api/test/scheduling-plans.test.ts @@ -7,7 +7,8 @@ import { createForm, type FormEnvironment } from "@coss/forms"; import { CreateFormRequest } from "@coss/forms/schemas"; import { testFormChecks } from "@coss/forms/testing"; import { memoryLimiter } from "@coss/ratelimit/testing"; -import { chargeDueHostFees, countLiveHosts, createHost } from "@coss/scheduling"; +import { chargeDueHostFees, countLiveHosts, createBooking, createHost } from "@coss/scheduling"; +import { testSchedulingDeps } from "@coss/scheduling/testing"; import { buildTestPdfBase64 } from "@coss/sign/testing"; import { createTestTenant, type TestTenant } from "@coss/tenancy/testing"; import { apiRequest, createTestApp, errorOf, type TestApp } from "./helpers.ts"; @@ -171,6 +172,60 @@ describe("Scheduling's plan gates", () => { expect((await keyed(lower, "another-key", { ...body, start: at("10:00") })).status).toBe(402); }); + test("an expired key makes something new, so it meets the plan", async () => { + const free = await createTestTenant(); + const app = enforced(); + const eventType = await bookable(app, free.liveKey); + const body = { event_type: eventType, start: at("09:00"), attendee }; + const withKey = (path: string, payload: object) => + app.request(`/v1/scheduling${path}`, { + method: "POST", + headers: { + Authorization: `Bearer ${free.liveKey}`, + "Content-Type": "application/json", + "Idempotency-Key": "old-key", + }, + body: JSON.stringify(payload), + }); + expect((await withKey("/bookings", body)).status).toBe(201); + await pool.query( + "update scheduling_booking set created_at = now() - interval '2 days' where organization_id = $1 and idempotency_key = 'old-key'", + [free.organization.id], + ); + expect((await withKey("/holds", { ...body, start: at("10:00") })).status).toBe(402); + }); + + test("the API's retry of a request made before the gate existed still replays", async () => { + const tenant = await onPlan("scale"); + const app = enforced(); + const eventType = await bookable(app, tenant.liveKey); + const start = at("09:00"); + // What a pre-deploy request hashed: no `gateRouting` in it. + const { booking } = await createBooking( + { + organizationId: tenant.organization.id, + projectId: tenant.project.id, + environmentId: tenant.production.id, + mode: "live", + }, + { eventType, startMs: new Date(start).getTime(), attendee, idempotencyKey: "pre-deploy" }, + testSchedulingDeps(), + { kind: "api_key", id: "key_test" }, + ); + const replay = await app.request("/v1/scheduling/bookings", { + method: "POST", + headers: { + Authorization: `Bearer ${tenant.liveKey}`, + "Content-Type": "application/json", + "Idempotency-Key": "pre-deploy", + }, + body: JSON.stringify({ event_type: eventType, start, attendee }), + }); + expect(replay.status).toBe(201); + expect(replay.headers.get("Idempotent-Replayed")).toBe("true"); + expect(((await replay.json()) as { id: string }).id).toBe(booking.id); + }); + test("the embedded host portal needs Scale", async () => { const free = await createTestTenant(); const app = enforced(); @@ -499,6 +554,16 @@ describe("holds reach every new live usage", () => { ); }); + test("new live hosts are refused: they would be a new monthly charge", async () => { + const tenant = await held(); + const app = enforced(); + await hold(tenant); + const refused = await call(app, tenant.liveKey, "POST", "/hosts", hostBody("a")); + expect(refused.status).toBe(402); + expect((await errorOf(refused)).code).toBe("billing_hold"); + expect((await call(app, tenant.sandboxKey, "POST", "/hosts", hostBody("a"))).status).toBe(201); + }); + test("the person on the booking page doesn't hear about the customer's billing", async () => { const tenant = await held(); const app = enforced(); diff --git a/apps/site/content/docs/(platform)/billing.mdx b/apps/site/content/docs/(platform)/billing.mdx index 159d64ae8..23a8f613d 100644 --- a/apps/site/content/docs/(platform)/billing.mdx +++ b/apps/site/content/docs/(platform)/billing.mdx @@ -72,8 +72,8 @@ after removing it. On a contract, add-ons are part of the contract. its inbox-days and new live Scheduling hosts past its host-days. Received mail past its allowance is dropped (and counted on **Received emails**). - If an invoice can't be collected, we retry it and email your billing contacts. Once it's overdue past the grace period (14 days unless your contract says otherwise), live sending is - paused (`402`, `billing_hold`) until it's paid. So are new live bookings, signature requests and - inboxes, and hosted forms stop accepting submissions. Sandbox keys keep working, scheduled emails + paused (`402`, `billing_hold`) until it's paid. So are new live bookings, Scheduling hosts, signature + requests and inboxes, and hosted forms stop accepting submissions. Sandbox keys keep working, scheduled emails wait rather than fail, and bookings already made stay. Mail still arrives, and monthly charges for inboxes, hosts and phone numbers you keep continue. - Owners can set a **spend limit**: when usage beyond your credit reaches it, live sending pauses diff --git a/apps/site/content/docs/(platform)/spend.mdx b/apps/site/content/docs/(platform)/spend.mdx index 2c4b8f761..586287b29 100644 --- a/apps/site/content/docs/(platform)/spend.mdx +++ b/apps/site/content/docs/(platform)/spend.mdx @@ -66,8 +66,8 @@ you change them) it emails the billing contacts, and the member it concerns, and start of the next month, or as soon as an owner raises (or removes) the budget. Checks run every few minutes, so spend can pass a budget by what is sent in between. Emails -already accepted before the pause still go out. New live bookings, signature requests and inboxes -in a project over budget are refused too, and its hosted forms stop accepting submissions. Mail +already accepted before the pause still go out. New live bookings, Scheduling hosts, signature +requests and inboxes in a project over budget are refused too, and its hosted forms stop accepting submissions. Mail still arrives and monthly charges for what you keep continue, so even a budget of $0 can't stop those. diff --git a/packages/scheduling/AGENTS.md b/packages/scheduling/AGENTS.md index fbcf0b024..769bb966f 100644 --- a/packages/scheduling/AGENTS.md +++ b/packages/scheduling/AGENTS.md @@ -215,14 +215,15 @@ Injected as `SchedulingDeps.plans`; without it nothing is enforced. What the pla - `scheduling.slots.second` (`limitSchedulingSlots` in the API): live slot lists and searches per organization. Gates take a connection of their own, so they never run inside a transaction: holding one connection while waiting for -another stalls the pool under load. They run before it, and `gateNewRequest` skips a retry of a key already used. +another stalls the pool under load. They run before it, and `gateNewRequest` skips a retry of a key still in its window (an expired key makes something +new, so it meets the plan). A person on a hosted page never meets a plan gate (Free's booking allowance aside): a session's attributes aren't checked again. Never gate the double-booking guarantee, calendars or series. A filter or a group size that exists keeps working whatever the plan; a downgrade is refused only for live `host_filter`s (the dashboard's preflight). `refuseNewBookingWhileHeld`, once the row is known to be new so a retry still answers with what it made: a billing hold -or the project's budget stops new live bookings, holds and series (402), and +or the project's budget stops new live bookings, holds and series (402; new hosts too, `refuseNewHostWhileHeld`), and the booker's page says only that the account can't take bookings. Confirming a hold and approving keep working: the customer may have taken payment for them. diff --git a/packages/scheduling/src/bookings.ts b/packages/scheduling/src/bookings.ts index c6d2117c8..8ffbac5e6 100644 --- a/packages/scheduling/src/bookings.ts +++ b/packages/scheduling/src/bookings.ts @@ -9,7 +9,11 @@ import { recordUsage } from "@coss/billing/metering"; import { db, type Executor, schema, type Transaction } from "@coss/db"; import { ID_PREFIXES, isId, newId } from "@coss/db/id"; -import { requestHash as hashRequest, withIdempotencyKey } from "@coss/db/idempotency"; +import { + requestHash as hashRequest, + IDEMPOTENCY_KEY_TTL_MS, + withIdempotencyKey, +} from "@coss/db/idempotency"; import { appendLogEntries } from "@coss/logs"; import { enqueueWebhookMessage } from "@coss/webhooks"; import { @@ -1422,8 +1426,9 @@ const idempotencyErrors = { }; /** - * What the plan decides about a new live request. A retry of a key already used skips it: whatever - * the plan says now, what it made is answered with (`withIdempotencyKey` checks that it matches). + * What the plan decides about a new live request. A retry of a key still in its window skips it: + * whatever the plan says now, what it made is answered with (`withIdempotencyKey` checks that it + * matches). An expired key makes something new, so it meets the plan. */ async function gateNewRequest( env: SchedulingEnvironment, @@ -1434,7 +1439,7 @@ async function gateNewRequest( if (env.mode !== "live" || !deps.plans) return; if (request.idempotencyKey) { const [made] = await db - .select({ id: opts.table.id }) + .select({ createdAt: opts.table.createdAt }) .from(opts.table) .where( and( @@ -1444,7 +1449,7 @@ async function gateNewRequest( ), ) .limit(1); - if (made) return; + if (made && nowOf(deps).getTime() - made.createdAt.getTime() <= IDEMPOTENCY_KEY_TTL_MS) return; } if (opts.hold) await requireLiveFeature(env, deps.plans, "schedulingHolds"); if (request.gateRouting) await requireRouting(env, deps.plans, request.attributes); @@ -1485,7 +1490,11 @@ async function book( ttlSeconds, }; const context: NewBookingContext = { env, eventType, deps, actor, now }; - const hash = hashRequest({ wantHold, request: { ...request, idempotencyKey: undefined } }); + const hash = hashRequest({ + wantHold, + // `gateRouting` isn't part of what was asked: keep hashes equal to those already stored. + request: { ...request, idempotencyKey: undefined, gateRouting: undefined }, + }); const checked = await withLiveCheck(env, candidates, plan, request.idempotencyKey ?? hash, deps); const run = (tx: Transaction, key: string | null) => @@ -2760,7 +2769,9 @@ export async function createSeries( const status = chooseStatus(eventType, false); const ttlSeconds = ttlFor(eventType, status); const rule = slotRuleOf(eventType, durationMinutes); - const hash = hashRequest({ series: { ...request, idempotencyKey: undefined } }); + const hash = hashRequest({ + series: { ...request, idempotencyKey: undefined, gateRouting: undefined }, + }); const context: NewBookingContext = { env, eventType, deps, actor, now }; const memberRanking = candidates.members.length > 0 ? ranked(candidates, hash) : []; diff --git a/packages/scheduling/src/hosts.ts b/packages/scheduling/src/hosts.ts index 81aab6a0c..6bbb9aa7c 100644 --- a/packages/scheduling/src/hosts.ts +++ b/packages/scheduling/src/hosts.ts @@ -23,6 +23,7 @@ import { } from "drizzle-orm"; import { isValidTimeZone } from "./engine/time.ts"; import { SchedulingError } from "./errors.ts"; +import { refuseNewHostWhileHeld } from "./plan.ts"; import type { CreateHostRequestOutput, HostLocationOutput, @@ -144,6 +145,7 @@ export async function createHost( refuseLinksInAddresses(params.locations); await screenSetup(env, params.name, opts.screen, "name"); await screenLocations(env, params.locations, opts.screen); + await refuseNewHostWhileHeld(env); const live = env.mode === "live"; const now = opts.now ?? new Date(); const pricedFrom = meterPricedFrom(HOST_FEE_METER) ?? now; diff --git a/packages/scheduling/src/plan.ts b/packages/scheduling/src/plan.ts index 8c46b79ac..333b13dd1 100644 --- a/packages/scheduling/src/plan.ts +++ b/packages/scheduling/src/plan.ts @@ -34,6 +34,23 @@ export async function requireRouting( } } +/** A new live host is a new monthly charge: an overdue invoice, spend limit or project budget stops it. */ +export async function refuseNewHostWhileHeld(env: SchedulingEnvironment): Promise { + if (env.mode !== "live") return; + if (await isOnBillingHold(env.organizationId)) { + throw new SchedulingError( + "billing_hold", + "New live hosts are paused: your organization has an overdue invoice or reached its spend " + + "limit. Sandbox keys still work.", + ); + } + const held = await budgetHoldFor({ + organizationId: env.organizationId, + projectId: env.projectId, + }); + if (held) throw new SchedulingError("budget_exceeded", BUDGET_EXCEEDED_MESSAGES[held]); +} + /** * A live new booking is usage: an overdue invoice, spend limit or project budget stops it. Called * once the row is known to be new, so a retry of a booking already made still answers with it. From 2f6fdc0c15047f36c0ba8803ad3a4f844f873dbc Mon Sep 17 00:00:00 2001 From: Bailey Pumfleet Date: Wed, 4 Nov 2026 15:31:44 -0600 Subject: [PATCH 6/9] Address Codex review (round 4) Give a fee date to live hosts an older task created during a rollout, after the migration ran: without one they would never count against the limit or pay. --- apps/api/test/scheduling-plans.test.ts | 22 ++++++++++++++++++++++ packages/scheduling/AGENTS.md | 3 ++- packages/scheduling/src/hosts.ts | 21 +++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/apps/api/test/scheduling-plans.test.ts b/apps/api/test/scheduling-plans.test.ts index 1950f86aa..91a23eb37 100644 --- a/apps/api/test/scheduling-plans.test.ts +++ b/apps/api/test/scheduling-plans.test.ts @@ -423,6 +423,28 @@ describe("Scheduling's plan gates", () => { }); describe("the host fee", () => { + test("a live host an older task made during a rollout still counts and pays", async () => { + const tenant = await onPlan("scale"); + const app = enforced(); + expect((await call(app, tenant.liveKey, "POST", "/hosts", hostBody("a"))).status).toBe(201); + expect((await call(app, tenant.sandboxKey, "POST", "/hosts", hostBody("b"))).status).toBe(201); + await pool.query( + "update scheduling_host set next_fee_at = null where organization_id = $1 and uid = 'a'", + [tenant.organization.id], + ); + expect(await countLiveHosts(tenant.organization.id)).toBe(0); + const scope = { organizationIds: [tenant.organization.id] }; + await chargeDueHostFees({ now: new Date("2026-12-01T00:00:00Z"), scope }); + expect(await countLiveHosts(tenant.organization.id)).toBe(1); + const { rows } = await pool.query<{ uid: string; next_fee_at: Date | null }>( + "select uid, next_fee_at from scheduling_host where organization_id = $1 order by uid", + [tenant.organization.id], + ); + expect(rows[0]?.next_fee_at).toEqual(new Date("2027-01-01T00:00:00Z")); + // The sandbox host is never given one. + expect(rows[1]?.next_fee_at).toBeNull(); + }); + test("past a capped plan's allowance a new host is refused, an existing one written off", async () => { const tenant = await createTestTenant(); const refusing: UsageGate = { diff --git a/packages/scheduling/AGENTS.md b/packages/scheduling/AGENTS.md index 769bb966f..323d86cb2 100644 --- a/packages/scheduling/AGENTS.md +++ b/packages/scheduling/AGENTS.md @@ -196,7 +196,8 @@ aren't charged; reschedules are free; cancelled stays charged. `scheduling.hosts` is one unit per day a live host exists (`next_fee_at`, its presence is what makes a host live for the plan's count): on creation (reserved like a new inbox: past a capped plan's allowance it's refused), then -`chargeDueHostFees` (the worker's `scheduling-host-fees`), and `deleteHost` charges what's due (those two only ever write +`chargeDueHostFees` (the worker's `scheduling-host-fees`, which also gives a date to any live host without one, such as +one an older task made during a rollout), and `deleteHost` charges what's due (those two only ever write off past the allowance). Nothing is recorded before `meterPricedFrom` (2027-01-01): hosts wait with `next_fee_at` at that date. ## Plans and holds diff --git a/packages/scheduling/src/hosts.ts b/packages/scheduling/src/hosts.ts index 6bbb9aa7c..09cbc6812 100644 --- a/packages/scheduling/src/hosts.ts +++ b/packages/scheduling/src/hosts.ts @@ -396,6 +396,27 @@ export async function chargeDueHostFees( const now = opts.now ?? new Date(); const limit = opts.limit ?? 1000; return db.transaction(async (tx) => { + // A live host without a date never counts or pays. An older task can still create one while a + // deploy rolls out, after the migration that set them. Due at once if the meter is priced. + const pricedFrom = meterPricedFrom(HOST_FEE_METER) ?? now; + await tx + .update(schedulingHost) + .set({ nextFeeAt: pricedFrom > now ? pricedFrom : now }) + .where( + and( + isNull(schedulingHost.nextFeeAt), + opts.scope + ? inArray(schedulingHost.organizationId, opts.scope.organizationIds) + : undefined, + inArray( + schedulingHost.environmentId, + tx + .select({ id: schema.environment.id }) + .from(schema.environment) + .where(eq(schema.environment.mode, "live")), + ), + ), + ); const due = tx .select({ id: schedulingHost.id }) .from(schedulingHost) From bb6f6868662d08b16a6f5341e504b76ed13e4636 Mon Sep 17 00:00:00 2001 From: Bailey Pumfleet Date: Wed, 4 Nov 2026 15:41:18 -0600 Subject: [PATCH 7/9] Address Codex review (round 5) - Slot explanations check attribute routing and the plan's slot rate, like slots. - A scheduled downgrade changes what's in use only when it applies: searches, attributes, holds and the host portal read the plan in effect. Only setup that outlives the request, such as a host filter, is checked against the plan to come. --- apps/api/openapi.json | 21 ++++++++++ apps/api/src/routes/scheduling.ts | 7 +++- apps/api/test/scheduling-plans.test.ts | 51 ++++++++++++++++++++++++- apps/web/src/lib/billing.ts | 2 + packages/billing/src/enforcement.ts | 4 ++ packages/billing/src/gate.ts | 7 +++- packages/email/test/contacts.test.ts | 3 ++ packages/migrate/test/runner.test.ts | 3 ++ packages/scheduling/AGENTS.md | 4 ++ packages/scheduling/src/event-types.ts | 7 ++-- packages/scheduling/src/plan.ts | 20 +++++++++- packages/sms/test/notifications.test.ts | 3 ++ packages/sms/test/provisioning.test.ts | 6 +++ 13 files changed, 131 insertions(+), 7 deletions(-) diff --git a/apps/api/openapi.json b/apps/api/openapi.json index f2468d321..dc352516a 100644 --- a/apps/api/openapi.json +++ b/apps/api/openapi.json @@ -69211,6 +69211,27 @@ } } }, + "402": { + "description": "In live environments: the plan doesn't include this (`plan_feature_unavailable`), or it's past the plan's limit (`plan_limit_reached`).", + "headers": { + "X-Request-Id": { + "description": "Unique id of this request. Include it when contacting support.", + "schema": { + "type": "string", + "examples": [ + "req_01k7c8m4q2f3e9v6t5r1n0p8yz" + ] + } + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, "403": { "description": "The API key lacks the permission this endpoint needs (`permission_error`), or its organization is suspended (`organization_suspended`).", "headers": { diff --git a/apps/api/src/routes/scheduling.ts b/apps/api/src/routes/scheduling.ts index c73b1c1e7..87f26b4b5 100644 --- a/apps/api/src/routes/scheduling.ts +++ b/apps/api/src/routes/scheduling.ts @@ -539,6 +539,7 @@ export const explainSlotRoute = createRoute({ middleware: read(), request: { params: EventTypeParams, query: ExplainSlotQuery }, responses: { + 402: planRefused, 200: jsonDoc("Whether it can be booked, and why.", SlotExplanation), ...commonErrors, 404: notFound, @@ -1567,6 +1568,8 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA router.openapi(explainSlotRoute, async (c) => { const query = c.req.valid("query"); + await limitSchedulingSlots(c.var.rateLimiter, c.var.auth); + await requireRouting(c.var.auth, deps.plans, attributesParam(query.attributes)); const explained = await explainSlot(c.var.auth, { eventTypeRef: c.req.valid("param").event_type, startMs: instant(query.start, "start"), @@ -1609,7 +1612,9 @@ export function createSchedulingRoutes(configured: SchedulingDeps | null): OpenA slotsPerHost: body.slots_per_host, limit: body.limit, maxHosts: - mode === "live" ? await deps.plans?.limit(organizationId, "schedulingSearchHosts") : null, + mode === "live" + ? await deps.plans?.currentLimit(organizationId, "schedulingSearchHosts") + : null, }); return c.json( { diff --git a/apps/api/test/scheduling-plans.test.ts b/apps/api/test/scheduling-plans.test.ts index 91a23eb37..f59e9faae 100644 --- a/apps/api/test/scheduling-plans.test.ts +++ b/apps/api/test/scheduling-plans.test.ts @@ -161,6 +161,7 @@ describe("Scheduling's plan gates", () => { const lower = enforced({ plans: { ...createPlanGate(), + hasFeature: async () => false, requireFeature: async () => { throw new BillingError("plan_feature_unavailable", "Not on this plan."); }, @@ -279,6 +280,14 @@ describe("Scheduling's plan gates", () => { }); expect(onHold.status).toBe(402); expect((await errorOf(onHold)).code).toBe("plan_feature_unavailable"); + const attributes = encodeURIComponent('{"specialty":"dermatology"}'); + const explained = await call( + app, + pro.liveKey, + "GET", + `/event_types/${eventType}/slots/explain?start=${at("09:00")}&attributes=${attributes}`, + ); + expect(explained.status).toBe(402); const filtered = await call(app, pro.liveKey, "POST", "/event_types", { title: "Pool", kind: "pool", @@ -358,7 +367,7 @@ describe("Scheduling's plan gates", () => { expect((await search()).data).toHaveLength(3); // The same search with the plan's limit below the pool. - const small = enforced({ plans: { ...createPlanGate(), limit: async () => 2 } }); + const small = enforced({ plans: { ...createPlanGate(), currentLimit: async () => 2 } }); const res = await call(small, pro.liveKey, "POST", "/availability/search", { event_type: id, start: at("09:00"), @@ -401,6 +410,46 @@ describe("Scheduling's plan gates", () => { expect((await sessionOf(sandbox, sandbox.sandboxKey)).coss_attribution).toBe(false); }); + test("a scheduled downgrade changes what's in use only when it applies", async () => { + const tenant = await createTestTenant(); + await createTestContract( + tenant.organization.id, + { version: 1, plan: "scale" }, + { + effectiveFrom: new Date("2026-01-01T00:00:00Z"), + effectiveUntil: new Date("2099-01-01T00:00:00Z"), + }, + ); + await createTestContract( + tenant.organization.id, + { version: 1, plan: "pro" }, + { effectiveFrom: new Date("2099-01-01T00:00:00Z") }, + ); + const app = enforced(); + const gate = createPlanGate(); + expect(await gate.currentLimit(tenant.organization.id, "schedulingSearchHosts")).toBe(500); + expect(await gate.limit(tenant.organization.id, "schedulingSearchHosts")).toBe(25); + + const eventType = await bookable(app, tenant.liveKey); + const slots = await call( + app, + tenant.liveKey, + "GET", + `/event_types/${eventType}/slots?start=${at("09:00")}&end=${at("10:00")}&attributes=${encodeURIComponent('{"language":"es"}')}`, + ); + expect(slots.status).not.toBe(402); + expect((await call(app, tenant.liveKey, "POST", "/hosts/h1/sessions", {})).status).toBe(201); + // Setting up what the lower plan lacks is refused already: it would block the downgrade. + const filtered = await call(app, tenant.liveKey, "POST", "/event_types", { + title: "Pool", + kind: "pool", + duration_minutes: 30, + hosts: [], + host_filter: { language: "es" }, + }); + expect(filtered.status).toBe(402); + }); + test("a scheduled downgrade keeps the attribution off until it applies", async () => { const tenant = await createTestTenant(); await createTestContract( diff --git a/apps/web/src/lib/billing.ts b/apps/web/src/lib/billing.ts index 783021dff..a7c09c303 100644 --- a/apps/web/src/lib/billing.ts +++ b/apps/web/src/lib/billing.ts @@ -62,6 +62,8 @@ export function planGate(): PlanGate { requireCapacity: async (organizationId, limit, current, count) => (await gate()).requireCapacity(organizationId, limit, current, count), limit: async (organizationId, limit) => (await gate()).limit(organizationId, limit), + currentLimit: async (organizationId, limit) => + (await gate()).currentLimit(organizationId, limit), }; } diff --git a/packages/billing/src/enforcement.ts b/packages/billing/src/enforcement.ts index a209de22d..e8e46affa 100644 --- a/packages/billing/src/enforcement.ts +++ b/packages/billing/src/enforcement.ts @@ -97,6 +97,10 @@ export function createPlanGate(opts: { systemOrganizationId?: string | null } = if (exempt(organizationId)) return null; return (await entitlementsFor(organizationId)).limits[limit]; }, + async currentLimit(organizationId, limit) { + if (exempt(organizationId)) return null; + return (await billingStateAt(organizationId, new Date())).terms.entitlements.limits[limit]; + }, }; } diff --git a/packages/billing/src/gate.ts b/packages/billing/src/gate.ts index 5fdce9c4a..5967414b7 100644 --- a/packages/billing/src/gate.ts +++ b/packages/billing/src/gate.ts @@ -24,8 +24,13 @@ export interface PlanGate { current: number, count?: number, ): Promise; - /** The plan's limit (`null`: unlimited), for packages that combine it with their own caps. */ + /** + * The limit on what can be created now (`null`: unlimited), for packages that combine it with + * their own caps: while a downgrade is scheduled, the lower of both plans. + */ limit(organizationId: string, limit: LimitKey): Promise; + /** The limit of the plan in effect (`null`: unlimited), for reads and use rather than creation. */ + currentLimit(organizationId: string, limit: LimitKey): Promise; } export interface UsageGate { diff --git a/packages/email/test/contacts.test.ts b/packages/email/test/contacts.test.ts index cbc9023df..51535b297 100644 --- a/packages/email/test/contacts.test.ts +++ b/packages/email/test/contacts.test.ts @@ -67,6 +67,9 @@ function limitedTo(limit: number): PlanGate { async limit() { return limit; }, + async currentLimit() { + return limit; + }, async requireCapacity(_organizationId, _key, current, count = 1) { if (current + count > limit) { throw new BillingError("plan_limit_reached", `Your plan includes ${limit} contacts.`); diff --git a/packages/migrate/test/runner.test.ts b/packages/migrate/test/runner.test.ts index acebe2d68..ae10214dd 100644 --- a/packages/migrate/test/runner.test.ts +++ b/packages/migrate/test/runner.test.ts @@ -420,6 +420,9 @@ describe("runDueMigrations", () => { async limit() { return 1; }, + async currentLimit() { + return 1; + }, async requireCapacity(_organizationId, _key, current, count = 1) { if (current + count > 1) { throw Object.assign(new Error("Your plan includes 1 contact."), { diff --git a/packages/scheduling/AGENTS.md b/packages/scheduling/AGENTS.md index e4064d8b2..044a4738e 100644 --- a/packages/scheduling/AGENTS.md +++ b/packages/scheduling/AGENTS.md @@ -278,6 +278,10 @@ Injected as `SchedulingDeps.plans`; without it nothing is enforced. What the pla - `schedulingWhiteLabel` (`coss_attribution` in the client API's `/session`). - `scheduling.slots.second` (`limitSchedulingSlots` in the API): live slot lists and searches per organization. +Using a feature reads the plan in effect (`requireLiveFeature`, `currentLimit`): a scheduled downgrade changes nothing +until it applies. Setting up something that outlives the request (`requireLiveFeatureToCreate`, `requireCapacity`) is also +checked against the plan to come, like every creation in billing. + Gates take a connection of their own, so they never run inside a transaction: holding one connection while waiting for another stalls the pool under load. They run before it, and `gateNewRequest` skips a retry of a key still in its window (an expired key makes something new, so it meets the plan). diff --git a/packages/scheduling/src/event-types.ts b/packages/scheduling/src/event-types.ts index ba84b40b2..9619fbaa9 100644 --- a/packages/scheduling/src/event-types.ts +++ b/packages/scheduling/src/event-types.ts @@ -21,7 +21,7 @@ import { notZoomMeeting } from "./calendar/zoom.ts"; import { startingUnits } from "./engine/rank.ts"; import { SchedulingError } from "./errors.ts"; import { attributesMatch, checkTimeZone, type HostRow, resolveHost } from "./hosts.ts"; -import { requireLiveFeature } from "./plan.ts"; +import { requireLiveFeatureToCreate } from "./plan.ts"; import type { CreateEventTypeRequestOutput, EventTypeObjectOutput, @@ -445,7 +445,8 @@ export async function createEventType( opts: { screen?: SetupScreening | undefined; plans?: PlanGate | undefined } = {}, ): Promise { // `{}` is a filter too: it matches every active host. - if (params.host_filter != null) await requireLiveFeature(env, opts.plans, "schedulingRouting"); + if (params.host_filter != null) + await requireLiveFeatureToCreate(env, opts.plans, "schedulingRouting"); await checkGroupSeats(env, opts.plans, params.capacity); refuseLinks(params.title, "title"); refuseLinksInAddresses(params.locations); @@ -624,7 +625,7 @@ export async function updateEventType( params.host_filter != null && JSON.stringify(params.host_filter) !== JSON.stringify(current.hostFilter) ) { - await requireLiveFeature(scope, opts.plans, "schedulingRouting"); + await requireLiveFeatureToCreate(scope, opts.plans, "schedulingRouting"); } await checkGroupSeats(scope, opts.plans, params.capacity, current.capacity ?? 0); } diff --git a/packages/scheduling/src/plan.ts b/packages/scheduling/src/plan.ts index 333b13dd1..88e5471c5 100644 --- a/packages/scheduling/src/plan.ts +++ b/packages/scheduling/src/plan.ts @@ -14,11 +14,29 @@ type Environment = Pick & { mode?: SchedulingEnvironment["mode"]; }; -/** Throws a `BillingError` when a live environment's plan lacks the feature. */ +/** + * Using a feature: throws a `BillingError` when the plan in effect lacks it. A scheduled downgrade + * changes nothing until it applies: the customer has paid for the month. + */ export async function requireLiveFeature( env: Environment, plans: PlanGate | undefined, feature: FeatureKey, +): Promise { + if (env.mode !== "live" || !plans) return; + if (!(await plans.hasFeature(env.organizationId, feature))) { + await plans.requireFeature(env.organizationId, feature); + } +} + +/** + * Setting up something that outlives this request: what a scheduled downgrade would leave behind + * is refused now too, or it would block the downgrade. + */ +export async function requireLiveFeatureToCreate( + env: Environment, + plans: PlanGate | undefined, + feature: FeatureKey, ): Promise { if (env.mode === "live") await plans?.requireFeature(env.organizationId, feature); } diff --git a/packages/sms/test/notifications.test.ts b/packages/sms/test/notifications.test.ts index 670651d6d..0947be577 100644 --- a/packages/sms/test/notifications.test.ts +++ b/packages/sms/test/notifications.test.ts @@ -26,6 +26,9 @@ const anyPlan: PlanGate = { async limit() { return null; }, + async currentLimit() { + return null; + }, async requireCapacity() {}, }; diff --git a/packages/sms/test/provisioning.test.ts b/packages/sms/test/provisioning.test.ts index 0305368cf..4d7dd0513 100644 --- a/packages/sms/test/provisioning.test.ts +++ b/packages/sms/test/provisioning.test.ts @@ -44,6 +44,9 @@ function plansAllowing(numbers: number | null): PlanGate { async limit() { return numbers; }, + async currentLimit() { + return numbers; + }, async requireCapacity(_org, _limit, current) { if (numbers !== null && current >= numbers) { throw Object.assign(new Error("limit"), { code: "plan_limit_reached" }); @@ -362,6 +365,9 @@ test("plans cap numbers; released numbers stop sending; deletion closes the suba async limit() { return 1; }, + async currentLimit() { + return 1; + }, async requireCapacity(_org, _limit, current) { if (current >= 1) throw Object.assign(new Error("limit"), { code: "plan_limit_reached" }); }, From 3b426ce024fab47fab8c16ab36b511e037c657c0 Mon Sep 17 00:00:00 2001 From: Bailey Pumfleet Date: Wed, 4 Nov 2026 15:46:30 -0600 Subject: [PATCH 8/9] Address Codex review (round 6) An unchanged host filter is compared canonically: jsonb doesn't keep key order, so resending a filter that already exists could be refused as a change. --- apps/api/test/scheduling-plans.test.ts | 21 +++++++++++++++++++++ packages/scheduling/src/event-types.ts | 4 +++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/apps/api/test/scheduling-plans.test.ts b/apps/api/test/scheduling-plans.test.ts index f59e9faae..ec390aa9f 100644 --- a/apps/api/test/scheduling-plans.test.ts +++ b/apps/api/test/scheduling-plans.test.ts @@ -128,6 +128,27 @@ describe("Scheduling's plan gates", () => { expect(asked).toBe(2); }); + test("resending an unchanged host filter isn't a change, whatever its key order", async () => { + const free = await createTestTenant(); + const { app: unenforced } = createTestApp(); + const created = await call(unenforced, free.liveKey, "POST", "/event_types", { + title: "Pool", + kind: "pool", + duration_minutes: 30, + hosts: [], + host_filter: { language: "es", specialty: "dermatology" }, + }); + expect(created.status).toBe(201); + const { id } = (await created.json()) as { id: string }; + const app = enforced(); + const edit = (host_filter: Record) => + call(app, free.liveKey, "PATCH", `/event_types/${id}`, { host_filter, title: "Renamed" }); + // jsonb stores shorter keys first, so this is another order than the one read back. + expect((await edit({ specialty: "dermatology", language: "es" })).status).toBe(200); + expect((await edit({ language: "es", specialty: "dermatology" })).status).toBe(200); + expect((await edit({ language: "fr", specialty: "dermatology" })).status).toBe(402); + }); + test("an empty host_filter is a filter: it matches every active host", async () => { const pro = await onPlan("pro"); const app = enforced(); diff --git a/packages/scheduling/src/event-types.ts b/packages/scheduling/src/event-types.ts index 9619fbaa9..486e01fce 100644 --- a/packages/scheduling/src/event-types.ts +++ b/packages/scheduling/src/event-types.ts @@ -3,6 +3,7 @@ import type { PlanGate } from "@coss/billing/gate"; import { HipaaModeError, hipaaModeApplies } from "@coss/compliance"; import { db, type Executor, schema } from "@coss/db"; import { ID_PREFIXES, isId } from "@coss/db/id"; +import { canonicalJson } from "@coss/db/idempotency"; import { and, asc, @@ -623,7 +624,8 @@ export async function updateEventType( const current = await resolveEventType(db, scope, ref); if ( params.host_filter != null && - JSON.stringify(params.host_filter) !== JSON.stringify(current.hostFilter) + // Canonical: jsonb doesn't keep the order of an object's keys. + canonicalJson(params.host_filter) !== canonicalJson(current.hostFilter) ) { await requireLiveFeatureToCreate(scope, opts.plans, "schedulingRouting"); } From 0e1f3e5031a936935d00a651d230915ec6506dbf Mon Sep 17 00:00:00 2001 From: Bailey Pumfleet Date: Wed, 4 Nov 2026 16:34:28 -0600 Subject: [PATCH 9/9] Give the tests merged from main what the plan gate now has The calendar delegation test's fake gate needs hasFeature and currentLimit, and the dashboard's Scheduling actions now reach the usage gate's system organization. --- apps/web/test/scheduling-actions.test.ts | 5 ++++- packages/scheduling/test/delegation.test.ts | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/web/test/scheduling-actions.test.ts b/apps/web/test/scheduling-actions.test.ts index 7f7cd5a66..7f9e69a40 100644 --- a/apps/web/test/scheduling-actions.test.ts +++ b/apps/web/test/scheduling-actions.test.ts @@ -28,7 +28,10 @@ mock.module("@coss/tenancy", () => ({ })); const recordAudit = mock(async (_session: unknown, _input: unknown) => {}); mock.module("@/lib/audit", () => ({ recordAudit })); -mock.module("@/lib/billing", () => ({ planGate: () => ({}) })); +mock.module("@/lib/billing", () => ({ + planGate: () => ({}), + systemOrganizationId: async () => null, +})); mock.module("@/lib/scheduling", () => ({ schedulingDeps: () => ({ dataKeys: [], diff --git a/packages/scheduling/test/delegation.test.ts b/packages/scheduling/test/delegation.test.ts index 78fb7e4ac..33ec2e07c 100644 --- a/packages/scheduling/test/delegation.test.ts +++ b/packages/scheduling/test/delegation.test.ts @@ -171,6 +171,8 @@ describe("the plan", () => { }, requireCapacity: async () => {}, limit: async () => null, + currentLimit: async () => null, + hasFeature: async () => allowed, }, }; };