From ebd37617f74c46405bedbee1f5a9519c8eaa2e03 Mon Sep 17 00:00:00 2001 From: Rajat Date: Wed, 11 Nov 2026 00:01:00 +0530 Subject: [PATCH 1/3] fix(sequences): stop retry floods when sending is blocked and warn owners early When an organization's sending was stopped, the sequence worker hit the stop before the code that reschedules a failed send, so every scheduler tick retried the same deliveries and logged and reported the error again. Production saw ~25 "sending_paused" errors every 40 seconds for one team. - Sending paused for billing or abuse reasons: queued sends wait and check again every 15-20 minutes, resuming once the stop lifts. - Stops only the team can fix (no feedback connection, unverified owner): the sequence is auto-paused and the owners are emailed once. - Any other failure before the send backs off 5 minutes and counts toward the 5-in-a-row auto-pause, without using up a recipient's retries. Failed sends back off between retries. - Owners are emailed at 50%, 90% and 100% of the cloud test volume while an ESP has no healthy feedback connection (migration 0032). - GET /settings/feedback-requirement (and MCP get_feedback_requirement) powers a notice in Settings -> ESP and the new-ESP dialog. Co-Authored-By: Claude Opus 5.5 --- .../0032_organization_feedback_notice.sql | 1 + apps/api/drizzle/meta/0032_snapshot.json | 11715 ++++++++++++++++ apps/api/drizzle/meta/_journal.json | 7 + apps/api/openapi.json | 58 + .../process-ongoing-sequence.test.ts | 169 +- .../automation/process-ongoing-sequence.ts | 221 +- .../api/src/automation/sending-health.test.ts | 8 +- apps/api/src/automation/sending-health.ts | 53 +- apps/api/src/billing/domains.ts | 52 +- apps/api/src/db/schema-core.ts | 4 + .../feedback-requirement.test.ts | 150 + .../delivery-feedback/feedback-requirement.ts | 158 + .../src/delivery-feedback/feedback-routes.ts | 8 + apps/api/src/mail/worker.ts | 2 + apps/api/src/mcp/policy.ts | 1 + .../src/mcp/tools/delivery-feedback.test.ts | 1 + apps/api/src/mcp/tools/delivery-feedback.ts | 20 + apps/api/src/observability/instance.ts | 3 + apps/web/app/(dashboard)/settings/page.tsx | 14 + .../dashboard/auto-pause-banner.tsx | 29 +- .../dashboard/esp-configuration-dialog.tsx | 12 +- .../dashboard/feedback-requirement-notice.tsx | 38 + apps/web/lib/api.ts | 12 + packages/api-contract/src/contract.ts | 9 + packages/api-contract/src/mcp-parity.ts | 1 + packages/api-contract/src/schemas/feedback.ts | 11 + packages/email-blocks/src/types.ts | 6 +- 27 files changed, 12681 insertions(+), 82 deletions(-) create mode 100644 apps/api/drizzle/0032_organization_feedback_notice.sql create mode 100644 apps/api/drizzle/meta/0032_snapshot.json create mode 100644 apps/api/src/delivery-feedback/feedback-requirement.test.ts create mode 100644 apps/api/src/delivery-feedback/feedback-requirement.ts create mode 100644 apps/web/components/dashboard/feedback-requirement-notice.tsx diff --git a/apps/api/drizzle/0032_organization_feedback_notice.sql b/apps/api/drizzle/0032_organization_feedback_notice.sql new file mode 100644 index 00000000..4741e0f9 --- /dev/null +++ b/apps/api/drizzle/0032_organization_feedback_notice.sql @@ -0,0 +1 @@ +ALTER TABLE "organizations" ADD COLUMN "feedback_notice_percent" integer DEFAULT 0 NOT NULL; \ No newline at end of file diff --git a/apps/api/drizzle/meta/0032_snapshot.json b/apps/api/drizzle/meta/0032_snapshot.json new file mode 100644 index 00000000..964cb1bc --- /dev/null +++ b/apps/api/drizzle/meta/0032_snapshot.json @@ -0,0 +1,11715 @@ +{ + "id": "cc82591f-5929-4afa-a47c-a70ffd97e517", + "prevId": "ae0fb770-31c2-4064-a2bc-41a6452c0ac5", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.abuse_flags": { + "name": "abuse_flags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "flag_id": { + "name": "flag_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "sequence_email_id": { + "name": "sequence_email_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "resolved_by": { + "name": "resolved_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolution_note": { + "name": "resolution_note", + "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": { + "abuse_flags_status_created_at_idx": { + "name": "abuse_flags_status_created_at_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "abuse_flags_organization_kind_status_idx": { + "name": "abuse_flags_organization_kind_status_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "abuse_flags_organization_id_organizations_id_fk": { + "name": "abuse_flags_organization_id_organizations_id_fk", + "tableFrom": "abuse_flags", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "abuse_flags_team_id_teams_id_fk": { + "name": "abuse_flags_team_id_teams_id_fk", + "tableFrom": "abuse_flags", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "abuse_flags_sequence_email_id_sequence_emails_id_fk": { + "name": "abuse_flags_sequence_email_id_sequence_emails_id_fk", + "tableFrom": "abuse_flags", + "tableTo": "sequence_emails", + "columnsFrom": [ + "sequence_email_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "abuse_flags_flag_id_unique": { + "name": "abuse_flags_flag_id_unique", + "nullsNotDistinct": false, + "columns": [ + "flag_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "abuse_flags_flag_id_check": { + "name": "abuse_flags_flag_id_check", + "value": "\"abuse_flags\".\"flag_id\" ~ '^abf_'" + }, + "abuse_flags_kind_check": { + "name": "abuse_flags_kind_check", + "value": "\"abuse_flags\".\"kind\" IN ('signup', 'content', 'import', 'broadcast')" + }, + "abuse_flags_severity_check": { + "name": "abuse_flags_severity_check", + "value": "\"abuse_flags\".\"severity\" IN ('low', 'medium', 'high')" + }, + "abuse_flags_status_check": { + "name": "abuse_flags_status_check", + "value": "\"abuse_flags\".\"status\" IN ('open', 'released', 'confirmed')" + } + }, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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 with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "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 with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_account_user_id_idx": { + "name": "auth_account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_account_issuer_account_id_uidx": { + "name": "auth_account_issuer_account_id_uidx", + "columns": [ + { + "expression": "issuer", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "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.contact_custom_field_values": { + "name": "contact_custom_field_values", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_type": { + "name": "value_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_text": { + "name": "value_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "value_number": { + "name": "value_number", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "value_boolean": { + "name": "value_boolean", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "value_date": { + "name": "value_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "contact_custom_field_values_contact_key_idx": { + "name": "contact_custom_field_values_contact_key_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_custom_field_values_text_lookup_idx": { + "name": "contact_custom_field_values_text_lookup_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value_text", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_custom_field_values_number_lookup_idx": { + "name": "contact_custom_field_values_number_lookup_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_custom_field_values_boolean_lookup_idx": { + "name": "contact_custom_field_values_boolean_lookup_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value_boolean", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_custom_field_values_date_lookup_idx": { + "name": "contact_custom_field_values_date_lookup_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "value_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_custom_field_values_team_id_teams_id_fk": { + "name": "contact_custom_field_values_team_id_teams_id_fk", + "tableFrom": "contact_custom_field_values", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_custom_field_values_contact_id_contacts_id_fk": { + "name": "contact_custom_field_values_contact_id_contacts_id_fk", + "tableFrom": "contact_custom_field_values", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_event_matches": { + "name": "contact_event_matches", + "schema": "", + "columns": { + "contact_event_id": { + "name": "contact_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enrolled": { + "name": "enrolled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "contact_event_matches_contact_event_id_contact_events_id_fk": { + "name": "contact_event_matches_contact_event_id_contact_events_id_fk", + "tableFrom": "contact_event_matches", + "tableTo": "contact_events", + "columnsFrom": [ + "contact_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_event_matches_sequence_id_sequences_id_fk": { + "name": "contact_event_matches_sequence_id_sequences_id_fk", + "tableFrom": "contact_event_matches", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "contact_event_matches_contact_event_id_sequence_id_pk": { + "name": "contact_event_matches_contact_event_id_sequence_id_pk", + "columns": [ + "contact_event_id", + "sequence_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contact_events": { + "name": "contact_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backfill": { + "name": "backfill", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "triggers": { + "name": "triggers", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "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": { + "contact_events_team_id_idempotency_key_idx": { + "name": "contact_events_team_id_idempotency_key_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"contact_events\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "contact_events_contact_id_occurred_at_idx": { + "name": "contact_events_contact_id_occurred_at_idx", + "columns": [ + { + "expression": "contact_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": {} + }, + "contact_events_unprocessed_idx": { + "name": "contact_events_unprocessed_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"contact_events\".\"processed_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contact_events_team_id_teams_id_fk": { + "name": "contact_events_team_id_teams_id_fk", + "tableFrom": "contact_events", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "contact_events_contact_id_contacts_id_fk": { + "name": "contact_events_contact_id_contacts_id_fk", + "tableFrom": "contact_events", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contact_events_event_id_unique": { + "name": "contact_events_event_id_unique", + "nullsNotDistinct": false, + "columns": [ + "event_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "contact_events_event_id_check": { + "name": "contact_events_event_id_check", + "value": "\"contact_events\".\"event_id\" ~ '^evt_'" + } + }, + "isRLSEnabled": false + }, + "public.contacts": { + "name": "contacts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscribed": { + "name": "subscribed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "custom_fields": { + "name": "custom_fields", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "unsubscribe_token": { + "name": "unsubscribe_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "contacts_team_id_email_idx": { + "name": "contacts_team_id_email_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contacts_team_id_teams_id_fk": { + "name": "contacts_team_id_teams_id_fk", + "tableFrom": "contacts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contacts_contact_id_unique": { + "name": "contacts_contact_id_unique", + "nullsNotDistinct": false, + "columns": [ + "contact_id" + ] + }, + "contacts_unsubscribe_token_unique": { + "name": "contacts_unsubscribe_token_unique", + "nullsNotDistinct": false, + "columns": [ + "unsubscribe_token" + ] + } + }, + "policies": {}, + "checkConstraints": { + "contacts_contact_id_check": { + "name": "contacts_contact_id_check", + "value": "\"contacts\".\"contact_id\" ~ '^cnt_'" + } + }, + "isRLSEnabled": false + }, + "public.email_content_versions": { + "name": "email_content_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "version_id": { + "name": "version_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence_email_id": { + "name": "sequence_email_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": 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": false, + "default": "now()" + } + }, + "indexes": { + "email_content_versions_email_hash_idx": { + "name": "email_content_versions_email_hash_idx", + "columns": [ + { + "expression": "sequence_email_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "content_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_content_versions_team_id_teams_id_fk": { + "name": "email_content_versions_team_id_teams_id_fk", + "tableFrom": "email_content_versions", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_content_versions_sequence_email_id_sequence_emails_id_fk": { + "name": "email_content_versions_sequence_email_id_sequence_emails_id_fk", + "tableFrom": "email_content_versions", + "tableTo": "sequence_emails", + "columnsFrom": [ + "sequence_email_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_content_versions_version_id_unique": { + "name": "email_content_versions_version_id_unique", + "nullsNotDistinct": false, + "columns": [ + "version_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "email_content_versions_version_id_check": { + "name": "email_content_versions_version_id_check", + "value": "\"email_content_versions\".\"version_id\" ~ '^ver_'" + } + }, + "isRLSEnabled": false + }, + "public.email_deliveries": { + "name": "email_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "content_version_id": { + "name": "content_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_deliveries_team_id_teams_id_fk": { + "name": "email_deliveries_team_id_teams_id_fk", + "tableFrom": "email_deliveries", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_deliveries_sequence_id_sequences_id_fk": { + "name": "email_deliveries_sequence_id_sequences_id_fk", + "tableFrom": "email_deliveries", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_deliveries_contact_id_contacts_id_fk": { + "name": "email_deliveries_contact_id_contacts_id_fk", + "tableFrom": "email_deliveries", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_deliveries_email_id_sequence_emails_id_fk": { + "name": "email_deliveries_email_id_sequence_emails_id_fk", + "tableFrom": "email_deliveries", + "tableTo": "sequence_emails", + "columnsFrom": [ + "email_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_deliveries_content_version_id_email_content_versions_id_fk": { + "name": "email_deliveries_content_version_id_email_content_versions_id_fk", + "tableFrom": "email_deliveries", + "tableTo": "email_content_versions", + "columnsFrom": [ + "content_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_delivery_events": { + "name": "email_delivery_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "receipt_id": { + "name": "receipt_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "outbound_message_id": { + "name": "outbound_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_event_key": { + "name": "provider_event_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "normalized_recipient": { + "name": "normalized_recipient", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bounce_class": { + "name": "bounce_class", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "smtp_code": { + "name": "smtp_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enhanced_status_code": { + "name": "enhanced_status_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_mta": { + "name": "remote_mta", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "email_delivery_events_connection_id_provider_event_key_idx": { + "name": "email_delivery_events_connection_id_provider_event_key_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_event_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_delivery_events_team_id_occurred_at_idx": { + "name": "email_delivery_events_team_id_occurred_at_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_delivery_events_outbound_message_id_idx": { + "name": "email_delivery_events_outbound_message_id_idx", + "columns": [ + { + "expression": "outbound_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_delivery_events_receipt_id_esp_webhook_receipts_id_fk": { + "name": "email_delivery_events_receipt_id_esp_webhook_receipts_id_fk", + "tableFrom": "email_delivery_events", + "tableTo": "esp_webhook_receipts", + "columnsFrom": [ + "receipt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_delivery_events_connection_id_esp_feedback_connections_id_fk": { + "name": "email_delivery_events_connection_id_esp_feedback_connections_id_fk", + "tableFrom": "email_delivery_events", + "tableTo": "esp_feedback_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_delivery_events_team_id_teams_id_fk": { + "name": "email_delivery_events_team_id_teams_id_fk", + "tableFrom": "email_delivery_events", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_delivery_events_outbound_message_id_outbound_messages_id_fk": { + "name": "email_delivery_events_outbound_message_id_outbound_messages_id_fk", + "tableFrom": "email_delivery_events", + "tableTo": "outbound_messages", + "columnsFrom": [ + "outbound_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_delivery_events_event_id_unique": { + "name": "email_delivery_events_event_id_unique", + "nullsNotDistinct": false, + "columns": [ + "event_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "email_delivery_events_event_id_check": { + "name": "email_delivery_events_event_id_check", + "value": "\"email_delivery_events\".\"event_id\" ~ '^(evt|dev)_'" + } + }, + "isRLSEnabled": false + }, + "public.email_events": { + "name": "email_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "link": { + "name": "link", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "link_index": { + "name": "link_index", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bounce_type": { + "name": "bounce_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bounce_reason": { + "name": "bounce_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "email_events_team_id_teams_id_fk": { + "name": "email_events_team_id_teams_id_fk", + "tableFrom": "email_events", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_events_sequence_id_sequences_id_fk": { + "name": "email_events_sequence_id_sequences_id_fk", + "tableFrom": "email_events", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_events_contact_id_contacts_id_fk": { + "name": "email_events_contact_id_contacts_id_fk", + "tableFrom": "email_events", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_events_email_id_sequence_emails_id_fk": { + "name": "email_events_email_id_sequence_emails_id_fk", + "tableFrom": "email_events", + "tableTo": "sequence_emails", + "columnsFrom": [ + "email_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_suppression_actions": { + "name": "email_suppression_actions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "suppression_id": { + "name": "suppression_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_event_id": { + "name": "source_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "explanation": { + "name": "explanation", + "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_actions_suppression_id_created_at_idx": { + "name": "email_suppression_actions_suppression_id_created_at_idx", + "columns": [ + { + "expression": "suppression_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_suppression_actions_team_id_teams_id_fk": { + "name": "email_suppression_actions_team_id_teams_id_fk", + "tableFrom": "email_suppression_actions", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_suppression_actions_suppression_id_email_suppressions_id_fk": { + "name": "email_suppression_actions_suppression_id_email_suppressions_id_fk", + "tableFrom": "email_suppression_actions", + "tableTo": "email_suppressions", + "columnsFrom": [ + "suppression_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_suppression_actions_source_event_id_email_delivery_events_id_fk": { + "name": "email_suppression_actions_source_event_id_email_delivery_events_id_fk", + "tableFrom": "email_suppression_actions", + "tableTo": "email_delivery_events", + "columnsFrom": [ + "source_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "email_suppression_actions_actor_user_id_user_id_fk": { + "name": "email_suppression_actions_actor_user_id_user_id_fk", + "tableFrom": "email_suppression_actions", + "tableTo": "user", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.email_suppressions": { + "name": "email_suppressions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "suppression_id": { + "name": "suppression_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "normalized_recipient": { + "name": "normalized_recipient", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recipient_hash": { + "name": "recipient_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hash_key_version": { + "name": "hash_key_version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_event_id": { + "name": "source_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "first_suppressed_at": { + "name": "first_suppressed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_suppressed_at": { + "name": "last_suppressed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "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 + }, + "release_reason": { + "name": "release_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "email_suppressions_team_id_recipient_hash_idx": { + "name": "email_suppressions_team_id_recipient_hash_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "recipient_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "email_suppressions_team_id_active_idx": { + "name": "email_suppressions_team_id_active_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_suppressions_team_id_teams_id_fk": { + "name": "email_suppressions_team_id_teams_id_fk", + "tableFrom": "email_suppressions", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "email_suppressions_source_event_id_email_delivery_events_id_fk": { + "name": "email_suppressions_source_event_id_email_delivery_events_id_fk", + "tableFrom": "email_suppressions", + "tableTo": "email_delivery_events", + "columnsFrom": [ + "source_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "email_suppressions_released_by_user_id_fk": { + "name": "email_suppressions_released_by_user_id_fk", + "tableFrom": "email_suppressions", + "tableTo": "user", + "columnsFrom": [ + "released_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_suppressions_suppression_id_unique": { + "name": "email_suppressions_suppression_id_unique", + "nullsNotDistinct": false, + "columns": [ + "suppression_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "email_suppressions_suppression_id_check": { + "name": "email_suppressions_suppression_id_check", + "value": "\"email_suppressions\".\"suppression_id\" ~ '^sup_'" + } + }, + "isRLSEnabled": false + }, + "public.email_templates": { + "name": "email_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'marketing'" + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "email_templates_team_id_title_idx": { + "name": "email_templates_team_id_title_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "email_templates_team_id_teams_id_fk": { + "name": "email_templates_team_id_teams_id_fk", + "tableFrom": "email_templates", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "email_templates_template_id_unique": { + "name": "email_templates_template_id_unique", + "nullsNotDistinct": false, + "columns": [ + "template_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "email_templates_template_id_check": { + "name": "email_templates_template_id_check", + "value": "\"email_templates\".\"template_id\" ~ '^tpl_'" + }, + "email_templates_purpose_check": { + "name": "email_templates_purpose_check", + "value": "\"email_templates\".\"purpose\" in ('marketing', 'transactional')" + } + }, + "isRLSEnabled": false + }, + "public.esp_config_team_grants": { + "name": "esp_config_team_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "esp_config_id": { + "name": "esp_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "drain_until": { + "name": "drain_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "daily_limit": { + "name": "daily_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "monthly_limit": { + "name": "monthly_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "created_by_type": { + "name": "created_by_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_id": { + "name": "created_by_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": { + "esp_config_team_grants_non_revoked_team_idx": { + "name": "esp_config_team_grants_non_revoked_team_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"esp_config_team_grants\".\"status\" <> 'revoked'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "esp_config_team_grants_team_organization_fk": { + "name": "esp_config_team_grants_team_organization_fk", + "tableFrom": "esp_config_team_grants", + "tableTo": "teams", + "columnsFrom": [ + "team_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "esp_config_team_grants_esp_organization_fk": { + "name": "esp_config_team_grants_esp_organization_fk", + "tableFrom": "esp_config_team_grants", + "tableTo": "esp_configs", + "columnsFrom": [ + "esp_config_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "esp_config_team_grants_grant_id_unique": { + "name": "esp_config_team_grants_grant_id_unique", + "nullsNotDistinct": false, + "columns": [ + "grant_id" + ] + }, + "esp_config_team_grants_id_organization_id_unique": { + "name": "esp_config_team_grants_id_organization_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "esp_config_team_grants_id_team_esp_unique": { + "name": "esp_config_team_grants_id_team_esp_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "team_id", + "esp_config_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "esp_config_team_grants_public_id_check": { + "name": "esp_config_team_grants_public_id_check", + "value": "\"esp_config_team_grants\".\"grant_id\" ~ '^egr_'" + }, + "esp_config_team_grants_status_check": { + "name": "esp_config_team_grants_status_check", + "value": "\"esp_config_team_grants\".\"status\" IN ('active', 'draining', 'suspended', 'revoked')" + }, + "esp_config_team_grants_limit_check": { + "name": "esp_config_team_grants_limit_check", + "value": "(\"esp_config_team_grants\".\"daily_limit\" IS NULL OR \"esp_config_team_grants\".\"daily_limit\" >= 0)\n AND (\"esp_config_team_grants\".\"monthly_limit\" IS NULL OR \"esp_config_team_grants\".\"monthly_limit\" >= 0)" + }, + "esp_config_team_grants_created_by_type_check": { + "name": "esp_config_team_grants_created_by_type_check", + "value": "\"esp_config_team_grants\".\"created_by_type\" IN ('user', 'organization_key', 'system')" + } + }, + "isRLSEnabled": false + }, + "public.esp_configs": { + "name": "esp_configs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "esp_id": { + "name": "esp_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'smtp'" + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 587 + }, + "secure": { + "name": "secure", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "encrypted_secret": { + "name": "encrypted_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_name": { + "name": "from_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_send_rate": { + "name": "max_send_rate", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "daily_send_limit": { + "name": "daily_send_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "warmup_target_daily": { + "name": "warmup_target_daily", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "warmup_started_at": { + "name": "warmup_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "dkim_selector": { + "name": "dkim_selector", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "secret_version": { + "name": "secret_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "last_tested_at": { + "name": "last_tested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_status": { + "name": "last_test_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_test_error": { + "name": "last_test_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "drain_until": { + "name": "drain_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_at": { + "name": "retired_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": { + "esp_configs_organization_id_idx": { + "name": "esp_configs_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "esp_configs_team_id_idx": { + "name": "esp_configs_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "esp_configs_organization_id_organizations_id_fk": { + "name": "esp_configs_organization_id_organizations_id_fk", + "tableFrom": "esp_configs", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "esp_configs_team_id_teams_id_fk": { + "name": "esp_configs_team_id_teams_id_fk", + "tableFrom": "esp_configs", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "esp_configs_esp_id_unique": { + "name": "esp_configs_esp_id_unique", + "nullsNotDistinct": false, + "columns": [ + "esp_id" + ] + }, + "esp_configs_id_organization_id_unique": { + "name": "esp_configs_id_organization_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + }, + "esp_configs_id_team_id_unique": { + "name": "esp_configs_id_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "esp_configs_esp_id_check": { + "name": "esp_configs_esp_id_check", + "value": "\"esp_configs\".\"esp_id\" ~ '^esp_'" + }, + "esp_configs_owner_check": { + "name": "esp_configs_owner_check", + "value": "(\"esp_configs\".\"owner_scope\" = 'organization' AND \"esp_configs\".\"organization_id\" IS NOT NULL AND \"esp_configs\".\"team_id\" IS NULL)\n OR (\"esp_configs\".\"owner_scope\" = 'team' AND \"esp_configs\".\"organization_id\" IS NULL AND \"esp_configs\".\"team_id\" IS NOT NULL)" + }, + "esp_configs_status_check": { + "name": "esp_configs_status_check", + "value": "\"esp_configs\".\"status\" IN ('draft', 'active', 'suspended', 'draining', 'retired')" + } + }, + "isRLSEnabled": false + }, + "public.esp_feedback_connections": { + "name": "esp_feedback_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_scope": { + "name": "owner_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "esp_config_id": { + "name": "esp_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_credentials": { + "name": "encrypted_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_encrypted_credentials": { + "name": "previous_encrypted_credentials", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "previous_credential_expires_at": { + "name": "previous_credential_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expected_topic_arn": { + "name": "expected_topic_arn", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscription_confirmed_at": { + "name": "subscription_confirmed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "last_received_at": { + "name": "last_received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_verified_at": { + "name": "last_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled_at": { + "name": "disabled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "esp_feedback_connections_team_id_idx": { + "name": "esp_feedback_connections_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "esp_feedback_connections_esp_config_active_idx": { + "name": "esp_feedback_connections_esp_config_active_idx", + "columns": [ + { + "expression": "esp_config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"esp_feedback_connections\".\"esp_config_id\" is not null and \"esp_feedback_connections\".\"status\" not in ('retiring', 'disabled')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "esp_feedback_connections_organization_id_organizations_id_fk": { + "name": "esp_feedback_connections_organization_id_organizations_id_fk", + "tableFrom": "esp_feedback_connections", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "esp_feedback_connections_team_id_teams_id_fk": { + "name": "esp_feedback_connections_team_id_teams_id_fk", + "tableFrom": "esp_feedback_connections", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "esp_feedback_connections_esp_config_id_esp_configs_id_fk": { + "name": "esp_feedback_connections_esp_config_id_esp_configs_id_fk", + "tableFrom": "esp_feedback_connections", + "tableTo": "esp_configs", + "columnsFrom": [ + "esp_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "esp_feedback_connections_connection_id_unique": { + "name": "esp_feedback_connections_connection_id_unique", + "nullsNotDistinct": false, + "columns": [ + "connection_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "esp_feedback_connections_connection_id_check": { + "name": "esp_feedback_connections_connection_id_check", + "value": "\"esp_feedback_connections\".\"connection_id\" ~ '^whc_'" + }, + "esp_feedback_connections_owner_check": { + "name": "esp_feedback_connections_owner_check", + "value": "(\n \"esp_feedback_connections\".\"owner_scope\" = 'organization'\n AND \"esp_feedback_connections\".\"organization_id\" IS NOT NULL\n AND \"esp_feedback_connections\".\"team_id\" IS NULL\n ) OR (\n \"esp_feedback_connections\".\"owner_scope\" = 'team'\n AND \"esp_feedback_connections\".\"organization_id\" IS NULL\n AND \"esp_feedback_connections\".\"team_id\" IS NOT NULL\n )" + } + }, + "isRLSEnabled": false + }, + "public.esp_webhook_receipts": { + "name": "esp_webhook_receipts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "receipt_id": { + "name": "receipt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_request_id": { + "name": "provider_request_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body_sha256": { + "name": "body_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_payload": { + "name": "encrypted_payload", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "safe_headers": { + "name": "safe_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_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_code": { + "name": "last_error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "esp_webhook_receipts_status_next_attempt_idx": { + "name": "esp_webhook_receipts_status_next_attempt_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "esp_webhook_receipts_connection_id_provider_request_id_idx": { + "name": "esp_webhook_receipts_connection_id_provider_request_id_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_request_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "esp_webhook_receipts_connection_id_esp_feedback_connections_id_fk": { + "name": "esp_webhook_receipts_connection_id_esp_feedback_connections_id_fk", + "tableFrom": "esp_webhook_receipts", + "tableTo": "esp_feedback_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "esp_webhook_receipts_team_id_teams_id_fk": { + "name": "esp_webhook_receipts_team_id_teams_id_fk", + "tableFrom": "esp_webhook_receipts", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "esp_webhook_receipts_receipt_id_unique": { + "name": "esp_webhook_receipts_receipt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "receipt_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "esp_webhook_receipts_receipt_id_check": { + "name": "esp_webhook_receipts_receipt_id_check", + "value": "\"esp_webhook_receipts\".\"receipt_id\" ~ '^whr_'" + } + }, + "isRLSEnabled": false + }, + "public.event_definitions": { + "name": "event_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin": { + "name": "origin", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "properties": { + "name": "properties", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "last_properties": { + "name": "last_properties", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "first_seen_at": { + "name": "first_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_seen_at": { + "name": "last_seen_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": { + "event_definitions_team_id_name_idx": { + "name": "event_definitions_team_id_name_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "event_definitions_team_id_teams_id_fk": { + "name": "event_definitions_team_id_teams_id_fk", + "tableFrom": "event_definitions", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.jwks": { + "name": "jwks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "alg": { + "name": "alg", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "crv": { + "name": "crv", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mail_dispatch_outbox": { + "name": "mail_dispatch_outbox", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "dispatch_id": { + "name": "dispatch_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outbound_message_id": { + "name": "outbound_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "queue_name": { + "name": "queue_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "job_name": { + "name": "job_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "publish_attempts": { + "name": "publish_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "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()" + } + }, + "indexes": { + "mail_dispatch_outbox_due_idx": { + "name": "mail_dispatch_outbox_due_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mail_dispatch_outbox_outbound_message_id_outbound_messages_id_fk": { + "name": "mail_dispatch_outbox_outbound_message_id_outbound_messages_id_fk", + "tableFrom": "mail_dispatch_outbox", + "tableTo": "outbound_messages", + "columnsFrom": [ + "outbound_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "mail_dispatch_outbox_dispatch_id_unique": { + "name": "mail_dispatch_outbox_dispatch_id_unique", + "nullsNotDistinct": false, + "columns": [ + "dispatch_id" + ] + }, + "mail_dispatch_outbox_outbound_message_id_unique": { + "name": "mail_dispatch_outbox_outbound_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "outbound_message_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "mail_dispatch_outbox_dispatch_id_check": { + "name": "mail_dispatch_outbox_dispatch_id_check", + "value": "\"mail_dispatch_outbox\".\"dispatch_id\" ~ '^mdj_'" + }, + "mail_dispatch_outbox_state_check": { + "name": "mail_dispatch_outbox_state_check", + "value": "\"mail_dispatch_outbox\".\"state\" IN ('pending', 'publishing', 'published', 'cancelled')" + } + }, + "isRLSEnabled": false + }, + "public.media": { + "name": "media", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_lit_id": { + "name": "media_lit_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_name": { + "name": "file_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "caption": { + "name": "caption", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "media_team_id_media_lit_id_idx": { + "name": "media_team_id_media_lit_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "media_lit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_team_id_created_at_idx": { + "name": "media_team_id_created_at_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_team_id_teams_id_fk": { + "name": "media_team_id_teams_id_fk", + "tableFrom": "media", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "media_media_id_unique": { + "name": "media_media_id_unique", + "nullsNotDistinct": false, + "columns": [ + "media_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "media_media_id_check": { + "name": "media_media_id_check", + "value": "\"media\".\"media_id\" ~ '^med_'" + } + }, + "isRLSEnabled": false + }, + "public.media_references": { + "name": "media_references", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "media_id": { + "name": "media_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_internal_id": { + "name": "resource_internal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "resource_public_id": { + "name": "resource_public_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_resource_internal_id": { + "name": "parent_resource_internal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_resource_public_id": { + "name": "parent_resource_public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "media_references_resource_idx": { + "name": "media_references_resource_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_references_media_id_idx": { + "name": "media_references_media_id_idx", + "columns": [ + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "media_references_resource_media_idx": { + "name": "media_references_resource_media_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_internal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "media_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "media_references_team_id_teams_id_fk": { + "name": "media_references_team_id_teams_id_fk", + "tableFrom": "media_references", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "media_references_media_id_media_id_fk": { + "name": "media_references_media_id_media_id_fk", + "tableFrom": "media_references", + "tableTo": "media", + "columnsFrom": [ + "media_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_access_token": { + "name": "oauth_access_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_code_id": { + "name": "authorization_code_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "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 + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "confirmation": { + "name": "confirmation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_oauth_access_token_client_id_idx": { + "name": "auth_oauth_access_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_access_token_session_id_idx": { + "name": "auth_oauth_access_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_access_token_user_id_idx": { + "name": "auth_oauth_access_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_access_token_authorization_code_id_idx": { + "name": "auth_oauth_access_token_authorization_code_id_idx", + "columns": [ + { + "expression": "authorization_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_access_token_refresh_id_idx": { + "name": "auth_oauth_access_token_refresh_id_idx", + "columns": [ + { + "expression": "refresh_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_access_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_access_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_session_id_session_id_fk": { + "name": "oauth_access_token_session_id_session_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_token_user_id_user_id_fk": { + "name": "oauth_access_token_user_id_user_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_token_refresh_id_oauth_refresh_token_id_fk": { + "name": "oauth_access_token_refresh_id_oauth_refresh_token_id_fk", + "tableFrom": "oauth_access_token", + "tableTo": "oauth_refresh_token", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_token_token_unique": { + "name": "oauth_access_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client": { + "name": "oauth_client", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_discovery_id": { + "name": "client_discovery_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "skip_consent": { + "name": "skip_consent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enable_end_session": { + "name": "enable_end_session", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "client_credentials_scopes": { + "name": "client_credentials_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contacts": { + "name": "contacts", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "tos": { + "name": "tos", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "policy": { + "name": "policy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_id": { + "name": "software_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_version": { + "name": "software_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "software_statement": { + "name": "software_statement", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "post_logout_redirect_uris": { + "name": "post_logout_redirect_uris", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_uri": { + "name": "backchannel_logout_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "backchannel_logout_session_required": { + "name": "backchannel_logout_session_required", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "application_type": { + "name": "application_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks": { + "name": "jwks", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "jwks_uri": { + "name": "jwks_uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "grant_types": { + "name": "grant_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "response_types": { + "name": "response_types", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public": { + "name": "public", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "require_pkce": { + "name": "require_pkce", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "dpop_bound_access_tokens": { + "name": "dpop_bound_access_tokens", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_oauth_client_user_id_idx": { + "name": "auth_oauth_client_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_client_user_id_user_id_fk": { + "name": "oauth_client_user_id_user_id_fk", + "tableFrom": "oauth_client", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_client_client_id_unique": { + "name": "oauth_client_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client_assertion": { + "name": "oauth_client_assertion", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_client_resource": { + "name": "oauth_client_resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "auth_oauth_client_resource_client_id_idx": { + "name": "auth_oauth_client_resource_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_client_resource_resource_id_idx": { + "name": "auth_oauth_client_resource_resource_id_idx", + "columns": [ + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_client_resource_client_id_resource_id_idx": { + "name": "auth_oauth_client_resource_client_id_resource_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_client_resource_client_id_oauth_client_client_id_fk": { + "name": "oauth_client_resource_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_client_resource", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_client_resource_resource_id_oauth_resource_identifier_fk": { + "name": "oauth_client_resource_resource_id_oauth_resource_identifier_fk", + "tableFrom": "oauth_client_resource", + "tableTo": "oauth_resource", + "columnsFrom": [ + "resource_id" + ], + "columnsTo": [ + "identifier" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_consent": { + "name": "oauth_consent", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_oauth_consent_client_id_idx": { + "name": "auth_oauth_consent_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_consent_user_id_idx": { + "name": "auth_oauth_consent_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_consent_client_id_oauth_client_client_id_fk": { + "name": "oauth_consent_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consent_user_id_user_id_fk": { + "name": "oauth_consent_user_id_user_id_fk", + "tableFrom": "oauth_consent", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_post_login_team_selections": { + "name": "oauth_post_login_team_selections", + "schema": "", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_post_login_team_selections_session_id_session_id_fk": { + "name": "oauth_post_login_team_selections_session_id_session_id_fk", + "tableFrom": "oauth_post_login_team_selections", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_post_login_team_selections_team_id_teams_id_fk": { + "name": "oauth_post_login_team_selections_team_id_teams_id_fk", + "tableFrom": "oauth_post_login_team_selections", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_refresh_token": { + "name": "oauth_refresh_token", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorization_code_id": { + "name": "authorization_code_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resources": { + "name": "resources", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "requested_user_info_claims": { + "name": "requested_user_info_claims", + "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 + }, + "revoked": { + "name": "revoked", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rotated_at": { + "name": "rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rotation_replay_response": { + "name": "rotation_replay_response", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rotation_replay_expires_at": { + "name": "rotation_replay_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "confirmation": { + "name": "confirmation", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_oauth_refresh_token_client_id_idx": { + "name": "auth_oauth_refresh_token_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_refresh_token_authorization_code_id_idx": { + "name": "auth_oauth_refresh_token_authorization_code_id_idx", + "columns": [ + { + "expression": "authorization_code_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_refresh_token_session_id_idx": { + "name": "auth_oauth_refresh_token_session_id_idx", + "columns": [ + { + "expression": "session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "auth_oauth_refresh_token_user_id_idx": { + "name": "auth_oauth_refresh_token_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "oauth_refresh_token_client_id_oauth_client_client_id_fk": { + "name": "oauth_refresh_token_client_id_oauth_client_client_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "oauth_client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_token_session_id_session_id_fk": { + "name": "oauth_refresh_token_session_id_session_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "session", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_token_user_id_user_id_fk": { + "name": "oauth_refresh_token_user_id_user_id_fk", + "tableFrom": "oauth_refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_refresh_token_token_unique": { + "name": "oauth_refresh_token_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.oauth_resource": { + "name": "oauth_resource", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token_ttl": { + "name": "access_token_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refresh_token_ttl": { + "name": "refresh_token_ttl", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signing_algorithm": { + "name": "signing_algorithm", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "signing_key_id": { + "name": "signing_key_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_scopes": { + "name": "allowed_scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "custom_claims": { + "name": "custom_claims", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dpop_bound_access_tokens_required": { + "name": "dpop_bound_access_tokens_required", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "disabled": { + "name": "disabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "policy_version": { + "name": "policy_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_resource_identifier_unique": { + "name": "oauth_resource_identifier_unique", + "nullsNotDistinct": false, + "columns": [ + "identifier" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ongoing_sequences": { + "name": "ongoing_sequences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "next_email_scheduled_time": { + "name": "next_email_scheduled_time", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "priority": { + "name": "priority", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "sent_email_ids": { + "name": "sent_email_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "pinned_versions": { + "name": "pinned_versions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "trigger_event": { + "name": "trigger_event", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "ongoing_sequences_sequence_id_contact_id_idx": { + "name": "ongoing_sequences_sequence_id_contact_id_idx", + "columns": [ + { + "expression": "sequence_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "contact_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ongoing_sequences_next_email_scheduled_time_idx": { + "name": "ongoing_sequences_next_email_scheduled_time_idx", + "columns": [ + { + "expression": "next_email_scheduled_time", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ongoing_sequences_sequence_id_next_email_idx": { + "name": "ongoing_sequences_sequence_id_next_email_idx", + "columns": [ + { + "expression": "sequence_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_email_scheduled_time", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ongoing_sequences_team_id_teams_id_fk": { + "name": "ongoing_sequences_team_id_teams_id_fk", + "tableFrom": "ongoing_sequences", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ongoing_sequences_sequence_id_sequences_id_fk": { + "name": "ongoing_sequences_sequence_id_sequences_id_fk", + "tableFrom": "ongoing_sequences", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ongoing_sequences_contact_id_contacts_id_fk": { + "name": "ongoing_sequences_contact_id_contacts_id_fk", + "tableFrom": "ongoing_sequences", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_api_keys": { + "name": "organization_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_api_key_id": { + "name": "organization_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_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()" + } + }, + "indexes": { + "organization_api_keys_organization_id_idx": { + "name": "organization_api_keys_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_api_keys_organization_id_organizations_id_fk": { + "name": "organization_api_keys_organization_id_organizations_id_fk", + "tableFrom": "organization_api_keys", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_api_keys_created_by_user_id_user_id_fk": { + "name": "organization_api_keys_created_by_user_id_user_id_fk", + "tableFrom": "organization_api_keys", + "tableTo": "user", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_api_keys_organization_api_key_id_unique": { + "name": "organization_api_keys_organization_api_key_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_api_key_id" + ] + }, + "organization_api_keys_key_hash_unique": { + "name": "organization_api_keys_key_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "key_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_api_keys_public_id_check": { + "name": "organization_api_keys_public_id_check", + "value": "\"organization_api_keys\".\"organization_api_key_id\" ~ '^oak_'" + } + }, + "isRLSEnabled": false + }, + "public.organization_audit_events": { + "name": "organization_audit_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "esp_config_id": { + "name": "esp_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "esp_grant_id": { + "name": "esp_grant_id", + "type": "uuid", + "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": { + "organization_audit_events_organization_id_created_at_idx": { + "name": "organization_audit_events_organization_id_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": {} + }, + "organization_audit_events_team_id_created_at_idx": { + "name": "organization_audit_events_team_id_created_at_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_audit_events_organization_id_organizations_id_fk": { + "name": "organization_audit_events_organization_id_organizations_id_fk", + "tableFrom": "organization_audit_events", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_delivery_policies": { + "name": "organization_delivery_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "default_esp_config_id": { + "name": "default_esp_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "auto_grant_default_esp": { + "name": "auto_grant_default_esp", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "default_daily_limit": { + "name": "default_daily_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "default_monthly_limit": { + "name": "default_monthly_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "aggregate_daily_limit": { + "name": "aggregate_daily_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "aggregate_monthly_limit": { + "name": "aggregate_monthly_limit", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "team_esp_enabled_by_default": { + "name": "team_esp_enabled_by_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "team_can_change_default": { + "name": "team_can_change_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": 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": { + "organization_delivery_policies_organization_id_organizations_id_fk": { + "name": "organization_delivery_policies_organization_id_organizations_id_fk", + "tableFrom": "organization_delivery_policies", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_delivery_policies_default_esp_fk": { + "name": "organization_delivery_policies_default_esp_fk", + "tableFrom": "organization_delivery_policies", + "tableTo": "esp_configs", + "columnsFrom": [ + "default_esp_config_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_delivery_policies_organization_id_unique": { + "name": "organization_delivery_policies_organization_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_delivery_policies_limit_check": { + "name": "organization_delivery_policies_limit_check", + "value": "(\"organization_delivery_policies\".\"default_daily_limit\" IS NULL OR \"organization_delivery_policies\".\"default_daily_limit\" >= 0)\n AND (\"organization_delivery_policies\".\"default_monthly_limit\" IS NULL OR \"organization_delivery_policies\".\"default_monthly_limit\" >= 0)\n AND (\"organization_delivery_policies\".\"aggregate_daily_limit\" IS NULL OR \"organization_delivery_policies\".\"aggregate_daily_limit\" >= 0)\n AND (\"organization_delivery_policies\".\"aggregate_monthly_limit\" IS NULL OR \"organization_delivery_policies\".\"aggregate_monthly_limit\" >= 0)" + } + }, + "isRLSEnabled": false + }, + "public.organization_esp_quota_reservations": { + "name": "organization_esp_quota_reservations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "reservation_id": { + "name": "reservation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outbound_message_id": { + "name": "outbound_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "day_period_start": { + "name": "day_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "month_period_start": { + "name": "month_period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "release_reason": { + "name": "release_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "committed_at": { + "name": "committed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "organization_esp_quota_reservations_outbound_message_id_outbound_messages_id_fk": { + "name": "organization_esp_quota_reservations_outbound_message_id_outbound_messages_id_fk", + "tableFrom": "organization_esp_quota_reservations", + "tableTo": "outbound_messages", + "columnsFrom": [ + "outbound_message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "organization_esp_quota_reservations_grant_id_esp_config_team_grants_id_fk": { + "name": "organization_esp_quota_reservations_grant_id_esp_config_team_grants_id_fk", + "tableFrom": "organization_esp_quota_reservations", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "organization_esp_quota_reservations_organization_id_organizations_id_fk": { + "name": "organization_esp_quota_reservations_organization_id_organizations_id_fk", + "tableFrom": "organization_esp_quota_reservations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "organization_esp_quota_reservations_grant_organization_fk": { + "name": "organization_esp_quota_reservations_grant_organization_fk", + "tableFrom": "organization_esp_quota_reservations", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "grant_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_esp_quota_reservations_reservation_id_unique": { + "name": "organization_esp_quota_reservations_reservation_id_unique", + "nullsNotDistinct": false, + "columns": [ + "reservation_id" + ] + }, + "organization_esp_quota_reservations_outbound_message_id_unique": { + "name": "organization_esp_quota_reservations_outbound_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "outbound_message_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_esp_quota_reservations_reservation_id_check": { + "name": "organization_esp_quota_reservations_reservation_id_check", + "value": "\"organization_esp_quota_reservations\".\"reservation_id\" ~ '^qrs_'" + }, + "organization_esp_quota_reservations_state_check": { + "name": "organization_esp_quota_reservations_state_check", + "value": "\"organization_esp_quota_reservations\".\"state\" IN ('reserved', 'committed', 'released')" + } + }, + "isRLSEnabled": false + }, + "public.organization_esp_usage_buckets": { + "name": "organization_esp_usage_buckets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "bucket_scope": { + "name": "bucket_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "grant_id": { + "name": "grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "period_type": { + "name": "period_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_start": { + "name": "period_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reserved_count": { + "name": "reserved_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "accepted_count": { + "name": "accepted_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": { + "organization_esp_usage_buckets_grant_period_idx": { + "name": "organization_esp_usage_buckets_grant_period_idx", + "columns": [ + { + "expression": "grant_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_esp_usage_buckets\".\"grant_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "organization_esp_usage_buckets_organization_period_idx": { + "name": "organization_esp_usage_buckets_organization_period_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_start", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"organization_esp_usage_buckets\".\"bucket_scope\" = 'organization'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_esp_usage_buckets_organization_id_organizations_id_fk": { + "name": "organization_esp_usage_buckets_organization_id_organizations_id_fk", + "tableFrom": "organization_esp_usage_buckets", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "organization_esp_usage_buckets_grant_id_esp_config_team_grants_id_fk": { + "name": "organization_esp_usage_buckets_grant_id_esp_config_team_grants_id_fk", + "tableFrom": "organization_esp_usage_buckets", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "organization_esp_usage_buckets_grant_organization_fk": { + "name": "organization_esp_usage_buckets_grant_organization_fk", + "tableFrom": "organization_esp_usage_buckets", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "grant_id", + "organization_id" + ], + "columnsTo": [ + "id", + "organization_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_esp_usage_buckets_scope_check": { + "name": "organization_esp_usage_buckets_scope_check", + "value": "(\n \"organization_esp_usage_buckets\".\"bucket_scope\" = 'grant' AND \"organization_esp_usage_buckets\".\"grant_id\" IS NOT NULL\n ) OR (\n \"organization_esp_usage_buckets\".\"bucket_scope\" = 'organization' AND \"organization_esp_usage_buckets\".\"grant_id\" IS NULL\n )" + }, + "organization_esp_usage_buckets_period_check": { + "name": "organization_esp_usage_buckets_period_check", + "value": "\"organization_esp_usage_buckets\".\"period_type\" IN ('day', 'month')" + }, + "organization_esp_usage_buckets_count_check": { + "name": "organization_esp_usage_buckets_count_check", + "value": "\"organization_esp_usage_buckets\".\"reserved_count\" >= 0 AND \"organization_esp_usage_buckets\".\"accepted_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.organization_members": { + "name": "organization_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "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": { + "organization_members_organization_id_user_id_idx": { + "name": "organization_members_organization_id_user_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_members_organization_id_organizations_id_fk": { + "name": "organization_members_organization_id_organizations_id_fk", + "tableFrom": "organization_members", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_members_user_id_user_id_fk": { + "name": "organization_members_user_id_user_id_fk", + "tableFrom": "organization_members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_members_role_check": { + "name": "organization_members_role_check", + "value": "\"organization_members\".\"role\" IN ('owner', 'admin', 'member')" + } + }, + "isRLSEnabled": false + }, + "public.organization_plan_policies": { + "name": "organization_plan_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "teams_limit_override": { + "name": "teams_limit_override", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "contacts_limit_override": { + "name": "contacts_limit_override", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "marketing_ramp_stage": { + "name": "marketing_ramp_stage", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "marketing_ramp_clean_days": { + "name": "marketing_ramp_clean_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "marketing_ramp_evaluated_at": { + "name": "marketing_ramp_evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "past_due_since": { + "name": "past_due_since", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pacing_exempt": { + "name": "pacing_exempt", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "signup_risk_score": { + "name": "signup_risk_score", + "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": { + "organization_plan_policies_organization_id_organizations_id_fk": { + "name": "organization_plan_policies_organization_id_organizations_id_fk", + "tableFrom": "organization_plan_policies", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organization_plan_policies_organization_id_unique": { + "name": "organization_plan_policies_organization_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organization_plan_policies_overrides_check": { + "name": "organization_plan_policies_overrides_check", + "value": "(\"organization_plan_policies\".\"teams_limit_override\" IS NULL OR \"organization_plan_policies\".\"teams_limit_override\" > 0)\n AND (\"organization_plan_policies\".\"contacts_limit_override\" IS NULL OR \"organization_plan_policies\".\"contacts_limit_override\" > 0)" + }, + "organization_plan_policies_ramp_check": { + "name": "organization_plan_policies_ramp_check", + "value": "\"organization_plan_policies\".\"marketing_ramp_stage\" BETWEEN 0 AND 3 AND \"organization_plan_policies\".\"marketing_ramp_clean_days\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "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": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pending_team_name": { + "name": "pending_team_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_notice_percent": { + "name": "feedback_notice_percent", + "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": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_organization_id_unique": { + "name": "organizations_organization_id_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "organizations_organization_id_check": { + "name": "organizations_organization_id_check", + "value": "\"organizations\".\"organization_id\" ~ '^org_'" + }, + "organizations_status_check": { + "name": "organizations_status_check", + "value": "\"organizations\".\"status\" IN ('pending_payment', 'active', 'suspended', 'abandoned', 'closed')" + } + }, + "isRLSEnabled": false + }, + "public.outbound_messages": { + "name": "outbound_messages", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "delivery_source_type": { + "name": "delivery_source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "esp_config_id": { + "name": "esp_config_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "esp_grant_id": { + "name": "esp_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "feedback_connection_id": { + "name": "feedback_connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "submission_key": { + "name": "submission_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "campaign_delivery_id": { + "name": "campaign_delivery_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "transactional_email_id": { + "name": "transactional_email_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "normalized_recipient": { + "name": "normalized_recipient", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rfc_message_id": { + "name": "rfc_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_message_id": { + "name": "provider_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "delivery_status": { + "name": "delivery_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "feedback_status": { + "name": "feedback_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "bounced_at": { + "name": "bounced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "complained_at": { + "name": "complained_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_event_at": { + "name": "last_event_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "outbound_messages_team_id_created_at_idx": { + "name": "outbound_messages_team_id_created_at_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbound_messages_connection_provider_msg_idx": { + "name": "outbound_messages_connection_provider_msg_idx", + "columns": [ + { + "expression": "feedback_connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "outbound_messages_team_id_recipient_created_at_idx": { + "name": "outbound_messages_team_id_recipient_created_at_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "normalized_recipient", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "outbound_messages_team_id_teams_id_fk": { + "name": "outbound_messages_team_id_teams_id_fk", + "tableFrom": "outbound_messages", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "outbound_messages_esp_config_id_esp_configs_id_fk": { + "name": "outbound_messages_esp_config_id_esp_configs_id_fk", + "tableFrom": "outbound_messages", + "tableTo": "esp_configs", + "columnsFrom": [ + "esp_config_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "outbound_messages_esp_grant_id_esp_config_team_grants_id_fk": { + "name": "outbound_messages_esp_grant_id_esp_config_team_grants_id_fk", + "tableFrom": "outbound_messages", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "esp_grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "outbound_messages_feedback_connection_id_esp_feedback_connections_id_fk": { + "name": "outbound_messages_feedback_connection_id_esp_feedback_connections_id_fk", + "tableFrom": "outbound_messages", + "tableTo": "esp_feedback_connections", + "columnsFrom": [ + "feedback_connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "outbound_messages_campaign_delivery_id_email_deliveries_id_fk": { + "name": "outbound_messages_campaign_delivery_id_email_deliveries_id_fk", + "tableFrom": "outbound_messages", + "tableTo": "email_deliveries", + "columnsFrom": [ + "campaign_delivery_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "outbound_messages_transactional_email_id_transactional_emails_id_fk": { + "name": "outbound_messages_transactional_email_id_transactional_emails_id_fk", + "tableFrom": "outbound_messages", + "tableTo": "transactional_emails", + "columnsFrom": [ + "transactional_email_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "outbound_messages_message_id_unique": { + "name": "outbound_messages_message_id_unique", + "nullsNotDistinct": false, + "columns": [ + "message_id" + ] + }, + "outbound_messages_submission_key_unique": { + "name": "outbound_messages_submission_key_unique", + "nullsNotDistinct": false, + "columns": [ + "submission_key" + ] + } + }, + "policies": {}, + "checkConstraints": { + "outbound_messages_message_id_check": { + "name": "outbound_messages_message_id_check", + "value": "\"outbound_messages\".\"message_id\" ~ '^msg_'" + }, + "outbound_messages_delivery_pin_check": { + "name": "outbound_messages_delivery_pin_check", + "value": "(\n \"outbound_messages\".\"delivery_source_type\" = 'team'\n AND \"outbound_messages\".\"esp_config_id\" IS NOT NULL\n AND \"outbound_messages\".\"esp_grant_id\" IS NULL\n ) OR (\n \"outbound_messages\".\"delivery_source_type\" = 'organization'\n AND \"outbound_messages\".\"esp_config_id\" IS NOT NULL\n AND \"outbound_messages\".\"esp_grant_id\" IS NOT NULL\n ) OR (\n \"outbound_messages\".\"delivery_source_type\" IN ('team', 'organization')\n AND \"outbound_messages\".\"esp_config_id\" IS NULL\n AND \"outbound_messages\".\"esp_grant_id\" IS NULL\n AND \"outbound_messages\".\"delivery_status\" <> 'queued'\n )" + } + }, + "isRLSEnabled": false + }, + "public.plan_send_reservations": { + "name": "plan_send_reservations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "outbound_message_id": { + "name": "outbound_message_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bucket_id": { + "name": "bucket_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "committed_at": { + "name": "committed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_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": { + "plan_send_reservations_outbound_uidx": { + "name": "plan_send_reservations_outbound_uidx", + "columns": [ + { + "expression": "outbound_message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plan_send_reservations_expiry_idx": { + "name": "plan_send_reservations_expiry_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plan_send_reservations_organization_id_organizations_id_fk": { + "name": "plan_send_reservations_organization_id_organizations_id_fk", + "tableFrom": "plan_send_reservations", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "plan_send_reservations_bucket_id_plan_send_usage_buckets_id_fk": { + "name": "plan_send_reservations_bucket_id_plan_send_usage_buckets_id_fk", + "tableFrom": "plan_send_reservations", + "tableTo": "plan_send_usage_buckets", + "columnsFrom": [ + "bucket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "plan_send_reservations_amount_check": { + "name": "plan_send_reservations_amount_check", + "value": "\"plan_send_reservations\".\"amount\" > 0" + }, + "plan_send_reservations_state_check": { + "name": "plan_send_reservations_state_check", + "value": "\"plan_send_reservations\".\"state\" IN ('reserved', 'committed', 'released')" + } + }, + "isRLSEnabled": false + }, + "public.plan_send_usage_buckets": { + "name": "plan_send_usage_buckets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "bucket_month": { + "name": "bucket_month", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "committed": { + "name": "committed", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "reserved": { + "name": "reserved", + "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": { + "plan_send_usage_buckets_organization_month_uidx": { + "name": "plan_send_usage_buckets_organization_month_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "bucket_month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plan_send_usage_buckets_organization_id_organizations_id_fk": { + "name": "plan_send_usage_buckets_organization_id_organizations_id_fk", + "tableFrom": "plan_send_usage_buckets", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "plan_send_usage_buckets_count_check": { + "name": "plan_send_usage_buckets_count_check", + "value": "\"plan_send_usage_buckets\".\"committed\" >= 0 AND \"plan_send_usage_buckets\".\"reserved\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.reputation_connections": { + "name": "reputation_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_secret": { + "name": "encrypted_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_label": { + "name": "account_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_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()" + }, + "alert_state": { + "name": "alert_state", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "subject_state": { + "name": "subject_state", + "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": { + "reputation_connections_team_id_provider_idx": { + "name": "reputation_connections_team_id_provider_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reputation_connections_next_sync_at_idx": { + "name": "reputation_connections_next_sync_at_idx", + "columns": [ + { + "expression": "next_sync_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reputation_connections_team_id_teams_id_fk": { + "name": "reputation_connections_team_id_teams_id_fk", + "tableFrom": "reputation_connections", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reputation_connections_provider_check": { + "name": "reputation_connections_provider_check", + "value": "\"reputation_connections\".\"provider\" IN ('google_postmaster', 'microsoft_snds')" + }, + "reputation_connections_status_check": { + "name": "reputation_connections_status_check", + "value": "\"reputation_connections\".\"status\" IN ('active', 'error')" + } + }, + "isRLSEnabled": false + }, + "public.reputation_daily_stats": { + "name": "reputation_daily_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "day": { + "name": "day", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "metrics": { + "name": "metrics", + "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": { + "reputation_daily_stats_connection_subject_day_idx": { + "name": "reputation_daily_stats_connection_subject_day_idx", + "columns": [ + { + "expression": "connection_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "subject", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "day", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reputation_daily_stats_connection_id_reputation_connections_id_fk": { + "name": "reputation_daily_stats_connection_id_reputation_connections_id_fk", + "tableFrom": "reputation_daily_stats", + "tableTo": "reputation_connections", + "columnsFrom": [ + "connection_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rules": { + "name": "rules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rule_id": { + "name": "rule_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "event_date_in_millis": { + "name": "event_date_in_millis", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "event_data": { + "name": "event_data", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_filter": { + "name": "event_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "rules_team_id_event_event_data_idx": { + "name": "rules_team_id_event_event_data_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_data", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "rules_team_id_teams_id_fk": { + "name": "rules_team_id_teams_id_fk", + "tableFrom": "rules", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "rules_sequence_id_sequences_id_fk": { + "name": "rules_sequence_id_sequences_id_fk", + "tableFrom": "rules", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "rules_rule_id_unique": { + "name": "rules_rule_id_unique", + "nullsNotDistinct": false, + "columns": [ + "rule_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "rules_rule_id_check": { + "name": "rules_rule_id_check", + "value": "\"rules\".\"rule_id\" ~ '^rule_'" + } + }, + "isRLSEnabled": false + }, + "public.segments": { + "name": "segments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "segment_id": { + "name": "segment_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": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "segments_team_id_name_idx": { + "name": "segments_team_id_name_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "segments_team_id_teams_id_fk": { + "name": "segments_team_id_teams_id_fk", + "tableFrom": "segments", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "segments_segment_id_unique": { + "name": "segments_segment_id_unique", + "nullsNotDistinct": false, + "columns": [ + "segment_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "segments_segment_id_check": { + "name": "segments_segment_id_check", + "value": "\"segments\".\"segment_id\" ~ '^seg_'" + } + }, + "isRLSEnabled": false + }, + "public.sending_domains": { + "name": "sending_domains", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "domain_id": { + "name": "domain_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "challenge_token_hash": { + "name": "challenge_token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "verified_at": { + "name": "verified_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 + }, + "next_check_at": { + "name": "next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failed_check_count": { + "name": "failed_check_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "first_failed_at": { + "name": "first_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": { + "sending_domains_organization_domain_uidx": { + "name": "sending_domains_organization_domain_uidx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sending_domains_organization_id_organizations_id_fk": { + "name": "sending_domains_organization_id_organizations_id_fk", + "tableFrom": "sending_domains", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sending_domains_domain_id_unique": { + "name": "sending_domains_domain_id_unique", + "nullsNotDistinct": false, + "columns": [ + "domain_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "sending_domains_domain_id_check": { + "name": "sending_domains_domain_id_check", + "value": "\"sending_domains\".\"domain_id\" ~ '^domain_'" + }, + "sending_domains_status_check": { + "name": "sending_domains_status_check", + "value": "\"sending_domains\".\"status\" IN ('pending', 'verified', 'revoked', 'failed')" + }, + "sending_domains_failed_check_count_check": { + "name": "sending_domains_failed_check_count_check", + "value": "\"sending_domains\".\"failed_check_count\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.sequence_emails": { + "name": "sequence_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email_id": { + "name": "email_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "delay_in_millis": { + "name": "delay_in_millis", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 86400000 + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_data": { + "name": "action_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "draft_subject": { + "name": "draft_subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "draft_content": { + "name": "draft_content", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "draft_updated_at": { + "name": "draft_updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_test_sent_at": { + "name": "last_test_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "content_scan_verdict": { + "name": "content_scan_verdict", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_scan_hash": { + "name": "content_scan_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_scan_reasons": { + "name": "content_scan_reasons", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "content_scanned_at": { + "name": "content_scanned_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sequence_emails_sequence_id_email_id_idx": { + "name": "sequence_emails_sequence_id_email_id_idx", + "columns": [ + { + "expression": "sequence_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sequence_emails_sequence_id_sequences_id_fk": { + "name": "sequence_emails_sequence_id_sequences_id_fk", + "tableFrom": "sequence_emails", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "sequence_emails_email_id_check": { + "name": "sequence_emails_email_id_check", + "value": "\"sequence_emails\".\"email_id\" ~ '^email_'" + } + }, + "isRLSEnabled": false + }, + "public.sequence_entries": { + "name": "sequence_entries", + "schema": "", + "columns": { + "sequence_id": { + "name": "sequence_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "first_entered_at": { + "name": "first_entered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_entered_at": { + "name": "last_entered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "entry_count": { + "name": "entry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": { + "sequence_entries_sequence_id_sequences_id_fk": { + "name": "sequence_entries_sequence_id_sequences_id_fk", + "tableFrom": "sequence_entries", + "tableTo": "sequences", + "columnsFrom": [ + "sequence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sequence_entries_contact_id_contacts_id_fk": { + "name": "sequence_entries_contact_id_contacts_id_fk", + "tableFrom": "sequence_entries", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "sequence_entries_sequence_id_contact_id_pk": { + "name": "sequence_entries_sequence_id_contact_id_pk", + "columns": [ + "sequence_id", + "contact_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sequences": { + "name": "sequences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence_id": { + "name": "sequence_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "delivery_source_intent": { + "name": "delivery_source_intent", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "delivery_source_type": { + "name": "delivery_source_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outbox_id": { + "name": "outbox_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "esp_grant_id": { + "name": "esp_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_data": { + "name": "trigger_data", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reentry": { + "name": "reentry", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'every_time'" + }, + "reentry_period_days": { + "name": "reentry_period_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "filter": { + "name": "filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "exclude_filter": { + "name": "exclude_filter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "emails_order": { + "name": "emails_order", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "entrants": { + "name": "entrants", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "report": { + "name": "report", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "view_in_browser": { + "name": "view_in_browser", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "web_published": { + "name": "web_published", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "web_slug": { + "name": "web_slug", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_seo_title": { + "name": "web_seo_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_seo_description": { + "name": "web_seo_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_og_image": { + "name": "web_og_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_noindex": { + "name": "web_noindex", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "web_canonical_url": { + "name": "web_canonical_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "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": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "sequences_team_id_web_slug_idx": { + "name": "sequences_team_id_web_slug_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "web_slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"sequences\".\"web_slug\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sequences_team_id_teams_id_fk": { + "name": "sequences_team_id_teams_id_fk", + "tableFrom": "sequences", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sequences_outbox_id_esp_configs_id_fk": { + "name": "sequences_outbox_id_esp_configs_id_fk", + "tableFrom": "sequences", + "tableTo": "esp_configs", + "columnsFrom": [ + "outbox_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "sequences_esp_grant_id_esp_config_team_grants_id_fk": { + "name": "sequences_esp_grant_id_esp_config_team_grants_id_fk", + "tableFrom": "sequences", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "esp_grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sequences_sequence_id_unique": { + "name": "sequences_sequence_id_unique", + "nullsNotDistinct": false, + "columns": [ + "sequence_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "sequences_sequence_id_check": { + "name": "sequences_sequence_id_check", + "value": "\"sequences\".\"sequence_id\" ~ '^seq_'" + }, + "sequences_delivery_pin_check": { + "name": "sequences_delivery_pin_check", + "value": "(\n \"sequences\".\"delivery_source_type\" IS NULL\n AND \"sequences\".\"outbox_id\" IS NULL\n AND \"sequences\".\"esp_grant_id\" IS NULL\n ) OR (\n \"sequences\".\"delivery_source_type\" = 'team'\n AND \"sequences\".\"outbox_id\" IS NOT NULL\n AND \"sequences\".\"esp_grant_id\" IS NULL\n ) OR (\n \"sequences\".\"delivery_source_type\" = 'organization'\n AND \"sequences\".\"outbox_id\" IS NOT NULL\n AND \"sequences\".\"esp_grant_id\" IS NOT NULL\n )" + } + }, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "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 + } + }, + "indexes": { + "auth_session_user_id_idx": { + "name": "auth_session_user_id_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.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mailing_address": { + "name": "mailing_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_enabled": { + "name": "web_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "web_handle": { + "name": "web_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_title": { + "name": "web_title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_description": { + "name": "web_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "web_signup_form_id": { + "name": "web_signup_form_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "tracking_domain": { + "name": "tracking_domain", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tracking_domain_status": { + "name": "tracking_domain_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tracking_domain_error": { + "name": "tracking_domain_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tracking_domain_verified_at": { + "name": "tracking_domain_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tracking_domain_checked_at": { + "name": "tracking_domain_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "tracking_domain_failures": { + "name": "tracking_domain_failures", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "retired_tracking_domains": { + "name": "retired_tracking_domains", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_team_id_teams_id_fk": { + "name": "settings_team_id_teams_id_fk", + "tableFrom": "settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "settings_web_signup_form_id_signup_forms_id_fk": { + "name": "settings_web_signup_form_id_signup_forms_id_fk", + "tableFrom": "settings", + "tableTo": "signup_forms", + "columnsFrom": [ + "web_signup_form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_team_id_unique": { + "name": "settings_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + }, + "settings_web_handle_unique": { + "name": "settings_web_handle_unique", + "nullsNotDistinct": false, + "columns": [ + "web_handle" + ] + }, + "settings_tracking_domain_unique": { + "name": "settings_tracking_domain_unique", + "nullsNotDistinct": false, + "columns": [ + "tracking_domain" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signup_confirmations": { + "name": "signup_confirmations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "form_id": { + "name": "form_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "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()" + } + }, + "indexes": { + "signup_confirmations_form_id_email_idx": { + "name": "signup_confirmations_form_id_email_idx", + "columns": [ + { + "expression": "form_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signup_confirmations_form_id_signup_forms_id_fk": { + "name": "signup_confirmations_form_id_signup_forms_id_fk", + "tableFrom": "signup_confirmations", + "tableTo": "signup_forms", + "columnsFrom": [ + "form_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "signup_confirmations_token_unique": { + "name": "signup_confirmations_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.signup_forms": { + "name": "signup_forms", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "form_id": { + "name": "form_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "double_opt_in": { + "name": "double_opt_in", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "redirect_url": { + "name": "redirect_url", + "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": { + "signup_forms_team_id_idx": { + "name": "signup_forms_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "signup_forms_team_id_teams_id_fk": { + "name": "signup_forms_team_id_teams_id_fk", + "tableFrom": "signup_forms", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "signup_forms_form_id_unique": { + "name": "signup_forms_form_id_unique", + "nullsNotDistinct": false, + "columns": [ + "form_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "signup_forms_form_id_check": { + "name": "signup_forms_form_id_check", + "value": "\"signup_forms\".\"form_id\" ~ '^frm_'" + } + }, + "isRLSEnabled": false + }, + "public.team_api_keys": { + "name": "team_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_api_key_id": { + "name": "team_api_key_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_prefix": { + "name": "key_prefix", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_type": { + "name": "created_by_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'user'" + }, + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "team_api_keys_team_id_idx": { + "name": "team_api_keys_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_api_keys_team_id_teams_id_fk": { + "name": "team_api_keys_team_id_teams_id_fk", + "tableFrom": "team_api_keys", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_api_keys_team_api_key_id_unique": { + "name": "team_api_keys_team_api_key_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_api_key_id" + ] + }, + "team_api_keys_key_hash_unique": { + "name": "team_api_keys_key_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "key_hash" + ] + } + }, + "policies": {}, + "checkConstraints": { + "team_api_keys_public_id_check": { + "name": "team_api_keys_public_id_check", + "value": "\"team_api_keys\".\"team_api_key_id\" ~ '^tak_'" + }, + "team_api_keys_created_by_type_check": { + "name": "team_api_keys_created_by_type_check", + "value": "\"team_api_keys\".\"created_by_type\" IN ('user', 'organization_key', 'system')" + } + }, + "isRLSEnabled": false + }, + "public.team_delivery_settings": { + "name": "team_delivery_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_esp_enabled": { + "name": "team_esp_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "team_can_change_default": { + "name": "team_can_change_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "default_source": { + "name": "default_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_team_esp_config_id": { + "name": "default_team_esp_config_id", + "type": "uuid", + "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": { + "team_delivery_settings_team_id_teams_id_fk": { + "name": "team_delivery_settings_team_id_teams_id_fk", + "tableFrom": "team_delivery_settings", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_delivery_settings_default_team_esp_fk": { + "name": "team_delivery_settings_default_team_esp_fk", + "tableFrom": "team_delivery_settings", + "tableTo": "esp_configs", + "columnsFrom": [ + "default_team_esp_config_id", + "team_id" + ], + "columnsTo": [ + "id", + "team_id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_delivery_settings_team_id_unique": { + "name": "team_delivery_settings_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "team_delivery_settings_default_source_check": { + "name": "team_delivery_settings_default_source_check", + "value": "\"team_delivery_settings\".\"default_source\" IS NULL OR \"team_delivery_settings\".\"default_source\" IN ('organization', 'team')" + } + }, + "isRLSEnabled": false + }, + "public.team_members": { + "name": "team_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "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 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": { + "team_members_team_id_user_id_idx": { + "name": "team_members_team_id_user_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "team_members_team_id_teams_id_fk": { + "name": "team_members_team_id_teams_id_fk", + "tableFrom": "team_members", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_user_id_fk": { + "name": "team_members_user_id_user_id_fk", + "tableFrom": "team_members", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "team_members_role_check": { + "name": "team_members_role_check", + "value": "\"team_members\".\"role\" IN ('admin', 'member')" + } + }, + "isRLSEnabled": false + }, + "public.team_sending_controls": { + "name": "team_sending_controls", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'normal'" + }, + "reason_code": { + "name": "reason_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'automatic'" + }, + "entered_at": { + "name": "entered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "evaluated_at": { + "name": "evaluated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "minimum_hold_until": { + "name": "minimum_hold_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "operator_user_id": { + "name": "operator_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "operator_reason": { + "name": "operator_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "overridden_at": { + "name": "overridden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "clean_evaluation_days": { + "name": "clean_evaluation_days", + "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": { + "team_sending_controls_team_id_teams_id_fk": { + "name": "team_sending_controls_team_id_teams_id_fk", + "tableFrom": "team_sending_controls", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "team_sending_controls_operator_user_id_user_id_fk": { + "name": "team_sending_controls_operator_user_id_user_id_fk", + "tableFrom": "team_sending_controls", + "tableTo": "user", + "columnsFrom": [ + "operator_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "team_sending_controls_team_id_unique": { + "name": "team_sending_controls_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "team_sending_controls_status_check": { + "name": "team_sending_controls_status_check", + "value": "\"team_sending_controls\".\"status\" IN ('normal', 'warned', 'marketing_paused', 'all_paused')" + }, + "team_sending_controls_source_check": { + "name": "team_sending_controls_source_check", + "value": "\"team_sending_controls\".\"source\" IN ('automatic', 'operator', 'abuse')" + }, + "team_sending_controls_clean_days_check": { + "name": "team_sending_controls_clean_days_check", + "value": "\"team_sending_controls\".\"clean_evaluation_days\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.teams": { + "name": "teams", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provisioning_request_hash": { + "name": "provisioning_request_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "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": { + "teams_organization_id_external_id_idx": { + "name": "teams_organization_id_external_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"teams\".\"external_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_organization_id_organizations_id_fk": { + "name": "teams_organization_id_organizations_id_fk", + "tableFrom": "teams", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "teams_team_id_unique": { + "name": "teams_team_id_unique", + "nullsNotDistinct": false, + "columns": [ + "team_id" + ] + }, + "teams_id_organization_id_unique": { + "name": "teams_id_organization_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "teams_team_id_check": { + "name": "teams_team_id_check", + "value": "\"teams\".\"team_id\" ~ '^team_'" + }, + "teams_status_check": { + "name": "teams_status_check", + "value": "\"teams\".\"status\" IN ('active', 'sending_suspended', 'archived')" + } + }, + "isRLSEnabled": false + }, + "public.transactional_emails": { + "name": "transactional_emails", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "txe_id": { + "name": "txe_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivery_source_type": { + "name": "delivery_source_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outbox_id": { + "name": "outbox_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "esp_grant_id": { + "name": "esp_grant_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "to_email": { + "name": "to_email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "from_email": { + "name": "from_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reply_to": { + "name": "reply_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "html": { + "name": "html", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "contact_id": { + "name": "contact_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "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 + }, + "open_count": { + "name": "open_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "click_count": { + "name": "click_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "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": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "transactional_emails_team_id_idempotency_key_idx": { + "name": "transactional_emails_team_id_idempotency_key_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"transactional_emails\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactional_emails_team_id_created_at_idx": { + "name": "transactional_emails_team_id_created_at_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "transactional_emails_team_id_status_idx": { + "name": "transactional_emails_team_id_status_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "transactional_emails_team_id_teams_id_fk": { + "name": "transactional_emails_team_id_teams_id_fk", + "tableFrom": "transactional_emails", + "tableTo": "teams", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "transactional_emails_outbox_id_esp_configs_id_fk": { + "name": "transactional_emails_outbox_id_esp_configs_id_fk", + "tableFrom": "transactional_emails", + "tableTo": "esp_configs", + "columnsFrom": [ + "outbox_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "transactional_emails_esp_grant_id_esp_config_team_grants_id_fk": { + "name": "transactional_emails_esp_grant_id_esp_config_team_grants_id_fk", + "tableFrom": "transactional_emails", + "tableTo": "esp_config_team_grants", + "columnsFrom": [ + "esp_grant_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "transactional_emails_contact_id_contacts_id_fk": { + "name": "transactional_emails_contact_id_contacts_id_fk", + "tableFrom": "transactional_emails", + "tableTo": "contacts", + "columnsFrom": [ + "contact_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "transactional_emails_txe_id_unique": { + "name": "transactional_emails_txe_id_unique", + "nullsNotDistinct": false, + "columns": [ + "txe_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "transactional_emails_txe_id_check": { + "name": "transactional_emails_txe_id_check", + "value": "\"transactional_emails\".\"txe_id\" ~ '^txe_'" + }, + "transactional_emails_delivery_pin_check": { + "name": "transactional_emails_delivery_pin_check", + "value": "(\n \"transactional_emails\".\"delivery_source_type\" = 'team'\n AND \"transactional_emails\".\"outbox_id\" IS NOT NULL\n AND \"transactional_emails\".\"esp_grant_id\" IS NULL\n ) OR (\n \"transactional_emails\".\"delivery_source_type\" = 'organization'\n AND \"transactional_emails\".\"outbox_id\" IS NOT NULL\n AND \"transactional_emails\".\"esp_grant_id\" IS NOT NULL\n )" + } + }, + "isRLSEnabled": false + }, + "public.trial_claims": { + "name": "trial_claims", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_email_fingerprint": { + "name": "verified_email_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "fingerprint_key_version": { + "name": "fingerprint_key_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trial_key": { + "name": "trial_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "checkout_attempt_id": { + "name": "checkout_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reserved'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "redeemed_at": { + "name": "redeemed_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": { + "trial_claims_user_trial_uidx": { + "name": "trial_claims_user_trial_uidx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trial_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"trial_claims\".\"status\" <> 'released'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "trial_claims_email_trial_uidx": { + "name": "trial_claims_email_trial_uidx", + "columns": [ + { + "expression": "verified_email_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "trial_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"trial_claims\".\"status\" <> 'released'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "trial_claims_checkout_attempt_idx": { + "name": "trial_claims_checkout_attempt_idx", + "columns": [ + { + "expression": "checkout_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "trial_claims_user_id_user_id_fk": { + "name": "trial_claims_user_id_user_id_fk", + "tableFrom": "trial_claims", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "trial_claims_organization_id_organizations_id_fk": { + "name": "trial_claims_organization_id_organizations_id_fk", + "tableFrom": "trial_claims", + "tableTo": "organizations", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "trial_claims_status_check": { + "name": "trial_claims_status_check", + "value": "\"trial_claims\".\"status\" IN ('reserved', 'redeemed', 'released')" + } + }, + "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 + }, + "default_organization_id": { + "name": "default_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "user_default_organization_id_organizations_id_fk": { + "name": "user_default_organization_id_organizations_id_fk", + "tableFrom": "user", + "tableTo": "organizations", + "columnsFrom": [ + "default_organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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 with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "auth_verification_identifier_idx": { + "name": "auth_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_catalog_revision_items": { + "name": "billing_catalog_revision_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "catalog_revision_id": { + "name": "catalog_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "offer_key": { + "name": "offer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_price_entry_id": { + "name": "billing_price_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "billing_catalog_revision_items_revision_key_uidx": { + "name": "billing_catalog_revision_items_revision_key_uidx", + "columns": [ + { + "expression": "catalog_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "offer_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_catalog_revision_items_revision_price_uidx": { + "name": "billing_catalog_revision_items_revision_price_uidx", + "columns": [ + { + "expression": "catalog_revision_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "billing_price_entry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_catalog_revision_items_catalog_revision_id_billing_catalog_revisions_id_fk": { + "name": "billing_catalog_revision_items_catalog_revision_id_billing_catalog_revisions_id_fk", + "tableFrom": "billing_catalog_revision_items", + "tableTo": "billing_catalog_revisions", + "columnsFrom": [ + "catalog_revision_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_catalog_revision_items_billing_price_entry_id_billing_price_entries_id_fk": { + "name": "billing_catalog_revision_items_billing_price_entry_id_billing_price_entries_id_fk", + "tableFrom": "billing_catalog_revision_items", + "tableTo": "billing_price_entries", + "columnsFrom": [ + "billing_price_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_catalog_revisions": { + "name": "billing_catalog_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "revision": { + "name": "revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "checkout_provider": { + "name": "checkout_provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_verification'" + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "activated_at": { + "name": "activated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "retired_at": { + "name": "retired_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_catalog_revisions_active_provider_uidx": { + "name": "billing_catalog_revisions_active_provider_uidx", + "columns": [ + { + "expression": "checkout_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_catalog_revisions\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_catalog_revisions_revision_unique": { + "name": "billing_catalog_revisions_revision_unique", + "nullsNotDistinct": false, + "columns": [ + "revision" + ] + } + }, + "policies": {}, + "checkConstraints": { + "billing_catalog_revisions_status_check": { + "name": "billing_catalog_revisions_status_check", + "value": "\"billing_catalog_revisions\".\"status\" IN ('pending_verification', 'active', 'retired', 'invalid', 'abandoned')" + }, + "billing_catalog_revisions_revision_check": { + "name": "billing_catalog_revisions_revision_check", + "value": "\"billing_catalog_revisions\".\"revision\" > 0" + } + }, + "isRLSEnabled": false + }, + "public.billing_checkout_attempts": { + "name": "billing_checkout_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "attempt_id": { + "name": "attempt_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billable_entity_id": { + "name": "billable_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "payer_id": { + "name": "payer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payer_email": { + "name": "payer_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "return_url": { + "name": "return_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "offer_key": { + "name": "offer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_plan": { + "name": "requested_plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_interval": { + "name": "requested_interval", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_price_entry_id": { + "name": "billing_price_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "quoted_amount_minor": { + "name": "quoted_amount_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quoted_currency": { + "name": "quoted_currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_customer_id": { + "name": "billing_customer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_checkout_session_id": { + "name": "provider_checkout_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_url_encrypted": { + "name": "checkout_url_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'creating'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "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": { + "billing_checkout_attempts_provider_session_uidx": { + "name": "billing_checkout_attempts_provider_session_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_checkout_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_checkout_attempts\".\"provider_checkout_session_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_checkout_attempts_idempotency_uidx": { + "name": "billing_checkout_attempts_idempotency_uidx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_checkout_attempts_entity_nonterminal_uidx": { + "name": "billing_checkout_attempts_entity_nonterminal_uidx", + "columns": [ + { + "expression": "billable_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_checkout_attempts\".\"status\" IN ('creating', 'open')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_checkout_attempts_billable_entity_id_organizations_id_fk": { + "name": "billing_checkout_attempts_billable_entity_id_organizations_id_fk", + "tableFrom": "billing_checkout_attempts", + "tableTo": "organizations", + "columnsFrom": [ + "billable_entity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_checkout_attempts_payer_id_user_id_fk": { + "name": "billing_checkout_attempts_payer_id_user_id_fk", + "tableFrom": "billing_checkout_attempts", + "tableTo": "user", + "columnsFrom": [ + "payer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_checkout_attempts_billing_price_entry_id_billing_price_entries_id_fk": { + "name": "billing_checkout_attempts_billing_price_entry_id_billing_price_entries_id_fk", + "tableFrom": "billing_checkout_attempts", + "tableTo": "billing_price_entries", + "columnsFrom": [ + "billing_price_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_checkout_attempts_billing_customer_id_billing_provider_customers_id_fk": { + "name": "billing_checkout_attempts_billing_customer_id_billing_provider_customers_id_fk", + "tableFrom": "billing_checkout_attempts", + "tableTo": "billing_provider_customers", + "columnsFrom": [ + "billing_customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_checkout_attempts_attempt_id_unique": { + "name": "billing_checkout_attempts_attempt_id_unique", + "nullsNotDistinct": false, + "columns": [ + "attempt_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "billing_checkout_attempts_status_check": { + "name": "billing_checkout_attempts_status_check", + "value": "\"billing_checkout_attempts\".\"status\" IN ('creating', 'open', 'completed', 'expired', 'abandoned', 'conflicted')" + }, + "billing_checkout_attempts_amount_check": { + "name": "billing_checkout_attempts_amount_check", + "value": "\"billing_checkout_attempts\".\"quoted_amount_minor\" > 0" + }, + "billing_checkout_attempts_plan_check": { + "name": "billing_checkout_attempts_plan_check", + "value": "\"billing_checkout_attempts\".\"requested_plan\" IN ('starter', 'pro', 'business')" + }, + "billing_checkout_attempts_interval_check": { + "name": "billing_checkout_attempts_interval_check", + "value": "\"billing_checkout_attempts\".\"requested_interval\" IN ('month', 'year')" + } + }, + "isRLSEnabled": false + }, + "public.billing_plan_change_attempts": { + "name": "billing_plan_change_attempts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "change_id": { + "name": "change_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billable_entity_id": { + "name": "billable_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payer_id": { + "name": "payer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_catalog_revision": { + "name": "current_catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "current_billing_price_entry_id": { + "name": "current_billing_price_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "current_plan": { + "name": "current_plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_interval": { + "name": "current_interval", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_catalog_revision": { + "name": "target_catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "target_billing_price_entry_id": { + "name": "target_billing_price_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_plan": { + "name": "target_plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_interval": { + "name": "target_interval", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_offer_key": { + "name": "target_offer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "effective_at": { + "name": "effective_at", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "proration_mode": { + "name": "proration_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_payment_id": { + "name": "provider_payment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_url_encrypted": { + "name": "payment_url_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'creating'" + }, + "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": { + "billing_plan_change_attempts_idempotency_uidx": { + "name": "billing_plan_change_attempts_idempotency_uidx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_plan_change_attempts_entity_nonterminal_uidx": { + "name": "billing_plan_change_attempts_entity_nonterminal_uidx", + "columns": [ + { + "expression": "billable_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_plan_change_attempts\".\"status\" IN ('creating', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_plan_change_attempts_billable_entity_id_organizations_id_fk": { + "name": "billing_plan_change_attempts_billable_entity_id_organizations_id_fk", + "tableFrom": "billing_plan_change_attempts", + "tableTo": "organizations", + "columnsFrom": [ + "billable_entity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_plan_change_attempts_subscription_id_billing_subscriptions_id_fk": { + "name": "billing_plan_change_attempts_subscription_id_billing_subscriptions_id_fk", + "tableFrom": "billing_plan_change_attempts", + "tableTo": "billing_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_plan_change_attempts_payer_id_user_id_fk": { + "name": "billing_plan_change_attempts_payer_id_user_id_fk", + "tableFrom": "billing_plan_change_attempts", + "tableTo": "user", + "columnsFrom": [ + "payer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_plan_change_attempts_current_billing_price_entry_id_billing_price_entries_id_fk": { + "name": "billing_plan_change_attempts_current_billing_price_entry_id_billing_price_entries_id_fk", + "tableFrom": "billing_plan_change_attempts", + "tableTo": "billing_price_entries", + "columnsFrom": [ + "current_billing_price_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_plan_change_attempts_target_billing_price_entry_id_billing_price_entries_id_fk": { + "name": "billing_plan_change_attempts_target_billing_price_entry_id_billing_price_entries_id_fk", + "tableFrom": "billing_plan_change_attempts", + "tableTo": "billing_price_entries", + "columnsFrom": [ + "target_billing_price_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_plan_change_attempts_change_id_unique": { + "name": "billing_plan_change_attempts_change_id_unique", + "nullsNotDistinct": false, + "columns": [ + "change_id" + ] + } + }, + "policies": {}, + "checkConstraints": { + "billing_plan_change_attempts_status_check": { + "name": "billing_plan_change_attempts_status_check", + "value": "\"billing_plan_change_attempts\".\"status\" IN ('creating', 'pending', 'succeeded', 'failed', 'conflicted')" + }, + "billing_plan_change_attempts_effective_at_check": { + "name": "billing_plan_change_attempts_effective_at_check", + "value": "\"billing_plan_change_attempts\".\"effective_at\" IN ('immediately', 'next_billing_date')" + }, + "billing_plan_change_attempts_proration_mode_check": { + "name": "billing_plan_change_attempts_proration_mode_check", + "value": "\"billing_plan_change_attempts\".\"proration_mode\" IN ('prorated_immediately', 'do_not_bill')" + }, + "billing_plan_change_attempts_current_plan_check": { + "name": "billing_plan_change_attempts_current_plan_check", + "value": "\"billing_plan_change_attempts\".\"current_plan\" IN ('starter', 'pro', 'business')" + }, + "billing_plan_change_attempts_target_plan_check": { + "name": "billing_plan_change_attempts_target_plan_check", + "value": "\"billing_plan_change_attempts\".\"target_plan\" IN ('starter', 'pro', 'business')" + } + }, + "isRLSEnabled": false + }, + "public.billing_plan_states": { + "name": "billing_plan_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "billable_entity_id": { + "name": "billable_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "active_subscription_id": { + "name": "active_subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "projection_version": { + "name": "projection_version", + "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": { + "billing_plan_states_billable_entity_id_organizations_id_fk": { + "name": "billing_plan_states_billable_entity_id_organizations_id_fk", + "tableFrom": "billing_plan_states", + "tableTo": "organizations", + "columnsFrom": [ + "billable_entity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_plan_states_active_subscription_id_billing_subscriptions_id_fk": { + "name": "billing_plan_states_active_subscription_id_billing_subscriptions_id_fk", + "tableFrom": "billing_plan_states", + "tableTo": "billing_subscriptions", + "columnsFrom": [ + "active_subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "billing_plan_states_billable_entity_id_unique": { + "name": "billing_plan_states_billable_entity_id_unique", + "nullsNotDistinct": false, + "columns": [ + "billable_entity_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.billing_price_entries": { + "name": "billing_price_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "offer_key": { + "name": "offer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_minor": { + "name": "amount_minor", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider_trial_days": { + "name": "provider_trial_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_product_id": { + "name": "provider_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "verified_at": { + "name": "verified_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_price_entries_provider_product_uidx": { + "name": "billing_price_entries_provider_product_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_product_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_price_entries_offer_key_idx": { + "name": "billing_price_entries_offer_key_idx", + "columns": [ + { + "expression": "offer_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_price_entries_amount_check": { + "name": "billing_price_entries_amount_check", + "value": "\"billing_price_entries\".\"amount_minor\" > 0" + }, + "billing_price_entries_trial_days_check": { + "name": "billing_price_entries_trial_days_check", + "value": "\"billing_price_entries\".\"provider_trial_days\" >= 0" + }, + "billing_price_entries_currency_check": { + "name": "billing_price_entries_currency_check", + "value": "\"billing_price_entries\".\"currency\" ~ '^[A-Z]{3}$'" + }, + "billing_price_entries_plan_check": { + "name": "billing_price_entries_plan_check", + "value": "\"billing_price_entries\".\"plan\" IN ('starter', 'pro', 'business')" + }, + "billing_price_entries_interval_check": { + "name": "billing_price_entries_interval_check", + "value": "\"billing_price_entries\".\"billing_interval\" IN ('month', 'year')" + } + }, + "isRLSEnabled": false + }, + "public.billing_provider_customers": { + "name": "billing_provider_customers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payer_id": { + "name": "payer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payer_email": { + "name": "payer_email", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "provider_customer_id": { + "name": "provider_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'creating'" + }, + "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": { + "billing_provider_customers_provider_payer_uidx": { + "name": "billing_provider_customers_provider_payer_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_provider_customers_provider_customer_uidx": { + "name": "billing_provider_customers_provider_customer_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_provider_customers\".\"provider_customer_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_provider_customers_idempotency_uidx": { + "name": "billing_provider_customers_idempotency_uidx", + "columns": [ + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_provider_customers_payer_id_user_id_fk": { + "name": "billing_provider_customers_payer_id_user_id_fk", + "tableFrom": "billing_provider_customers", + "tableTo": "user", + "columnsFrom": [ + "payer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_provider_customers_status_check": { + "name": "billing_provider_customers_status_check", + "value": "\"billing_provider_customers\".\"status\" IN ('creating', 'active', 'conflicted')" + } + }, + "isRLSEnabled": false + }, + "public.billing_reconciliation_jobs": { + "name": "billing_reconciliation_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checkout_attempt_id": { + "name": "checkout_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "plan_change_attempt_id": { + "name": "plan_change_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "subscription_id": { + "name": "subscription_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_customer_id": { + "name": "provider_customer_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'reconcile'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_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 + }, + "worker_id": { + "name": "worker_id", + "type": "text", + "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": { + "billing_reconciliation_jobs_live_checkout_uidx": { + "name": "billing_reconciliation_jobs_live_checkout_uidx", + "columns": [ + { + "expression": "checkout_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_reconciliation_jobs\".\"checkout_attempt_id\" IS NOT NULL AND \"billing_reconciliation_jobs\".\"status\" IN ('pending', 'processing', 'failed')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_reconciliation_jobs_live_plan_change_uidx": { + "name": "billing_reconciliation_jobs_live_plan_change_uidx", + "columns": [ + { + "expression": "plan_change_attempt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_reconciliation_jobs\".\"plan_change_attempt_id\" IS NOT NULL AND \"billing_reconciliation_jobs\".\"status\" IN ('pending', 'processing', 'failed')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_reconciliation_jobs_live_subscription_uidx": { + "name": "billing_reconciliation_jobs_live_subscription_uidx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_reconciliation_jobs\".\"subscription_id\" IS NOT NULL AND \"billing_reconciliation_jobs\".\"status\" IN ('pending', 'processing', 'failed')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_reconciliation_jobs_live_customer_uidx": { + "name": "billing_reconciliation_jobs_live_customer_uidx", + "columns": [ + { + "expression": "provider_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_reconciliation_jobs\".\"provider_customer_id\" IS NOT NULL AND \"billing_reconciliation_jobs\".\"status\" IN ('pending', 'processing', 'failed')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_reconciliation_jobs_checkout_attempt_id_billing_checkout_attempts_id_fk": { + "name": "billing_reconciliation_jobs_checkout_attempt_id_billing_checkout_attempts_id_fk", + "tableFrom": "billing_reconciliation_jobs", + "tableTo": "billing_checkout_attempts", + "columnsFrom": [ + "checkout_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_reconciliation_jobs_plan_change_attempt_id_billing_plan_change_attempts_id_fk": { + "name": "billing_reconciliation_jobs_plan_change_attempt_id_billing_plan_change_attempts_id_fk", + "tableFrom": "billing_reconciliation_jobs", + "tableTo": "billing_plan_change_attempts", + "columnsFrom": [ + "plan_change_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_reconciliation_jobs_subscription_id_billing_subscriptions_id_fk": { + "name": "billing_reconciliation_jobs_subscription_id_billing_subscriptions_id_fk", + "tableFrom": "billing_reconciliation_jobs", + "tableTo": "billing_subscriptions", + "columnsFrom": [ + "subscription_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_reconciliation_jobs_provider_customer_id_billing_provider_customers_id_fk": { + "name": "billing_reconciliation_jobs_provider_customer_id_billing_provider_customers_id_fk", + "tableFrom": "billing_reconciliation_jobs", + "tableTo": "billing_provider_customers", + "columnsFrom": [ + "provider_customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_reconciliation_jobs_exactly_one_subject": { + "name": "billing_reconciliation_jobs_exactly_one_subject", + "value": "((CASE WHEN \"billing_reconciliation_jobs\".\"checkout_attempt_id\" IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN \"billing_reconciliation_jobs\".\"plan_change_attempt_id\" IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN \"billing_reconciliation_jobs\".\"subscription_id\" IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN \"billing_reconciliation_jobs\".\"provider_customer_id\" IS NOT NULL THEN 1 ELSE 0 END)) = 1" + }, + "billing_reconciliation_jobs_status_check": { + "name": "billing_reconciliation_jobs_status_check", + "value": "\"billing_reconciliation_jobs\".\"status\" IN ('pending', 'processing', 'failed', 'completed', 'quarantined')" + }, + "billing_reconciliation_jobs_operation_check": { + "name": "billing_reconciliation_jobs_operation_check", + "value": "\"billing_reconciliation_jobs\".\"operation\" = 'reconcile' OR (\"billing_reconciliation_jobs\".\"operation\" = 'cancellation' AND \"billing_reconciliation_jobs\".\"subscription_id\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.billing_subscriptions": { + "name": "billing_subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "billable_entity_id": { + "name": "billable_entity_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "billing_customer_id": { + "name": "billing_customer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "payer_id": { + "name": "payer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "origin_checkout_attempt_id": { + "name": "origin_checkout_attempt_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_product_id": { + "name": "provider_product_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_price_entry_id": { + "name": "billing_price_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "catalog_revision": { + "name": "catalog_revision", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "offer_key": { + "name": "offer_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "current_period_starts_at": { + "name": "current_period_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_ends_at": { + "name": "current_period_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "paid_through_at": { + "name": "paid_through_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_entitlement_source": { + "name": "is_entitlement_source", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "provider_occurred_at": { + "name": "provider_occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "provider_version": { + "name": "provider_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_observed_at": { + "name": "last_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_reconciled_at": { + "name": "last_reconciled_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_subscriptions_provider_subscription_uidx": { + "name": "billing_subscriptions_provider_subscription_uidx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_subscriptions_entity_source_uidx": { + "name": "billing_subscriptions_entity_source_uidx", + "columns": [ + { + "expression": "billable_entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_subscriptions\".\"is_entitlement_source\" = true", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_subscriptions_billable_entity_id_organizations_id_fk": { + "name": "billing_subscriptions_billable_entity_id_organizations_id_fk", + "tableFrom": "billing_subscriptions", + "tableTo": "organizations", + "columnsFrom": [ + "billable_entity_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_subscriptions_billing_customer_id_billing_provider_customers_id_fk": { + "name": "billing_subscriptions_billing_customer_id_billing_provider_customers_id_fk", + "tableFrom": "billing_subscriptions", + "tableTo": "billing_provider_customers", + "columnsFrom": [ + "billing_customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_subscriptions_payer_id_user_id_fk": { + "name": "billing_subscriptions_payer_id_user_id_fk", + "tableFrom": "billing_subscriptions", + "tableTo": "user", + "columnsFrom": [ + "payer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_subscriptions_origin_checkout_attempt_id_billing_checkout_attempts_id_fk": { + "name": "billing_subscriptions_origin_checkout_attempt_id_billing_checkout_attempts_id_fk", + "tableFrom": "billing_subscriptions", + "tableTo": "billing_checkout_attempts", + "columnsFrom": [ + "origin_checkout_attempt_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "billing_subscriptions_billing_price_entry_id_billing_price_entries_id_fk": { + "name": "billing_subscriptions_billing_price_entry_id_billing_price_entries_id_fk", + "tableFrom": "billing_subscriptions", + "tableTo": "billing_price_entries", + "columnsFrom": [ + "billing_price_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_subscriptions_status_check": { + "name": "billing_subscriptions_status_check", + "value": "\"billing_subscriptions\".\"status\" IN ('pending', 'trialing', 'active', 'past_due', 'cancelled', 'expired')" + }, + "billing_subscriptions_plan_check": { + "name": "billing_subscriptions_plan_check", + "value": "\"billing_subscriptions\".\"plan\" IN ('starter', 'pro', 'business')" + }, + "billing_subscriptions_interval_check": { + "name": "billing_subscriptions_interval_check", + "value": "\"billing_subscriptions\".\"billing_interval\" IN ('month', 'year')" + } + }, + "isRLSEnabled": false + }, + "public.billing_webhook_events": { + "name": "billing_webhook_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_event_id": { + "name": "provider_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_attempt_id": { + "name": "checkout_attempt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_encrypted": { + "name": "payload_encrypted", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_key_version": { + "name": "payload_key_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified_key_version": { + "name": "verified_key_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_attempts": { + "name": "processing_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "available_at": { + "name": "available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_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 + }, + "worker_id": { + "name": "worker_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "billing_webhook_events_provider_event_uidx": { + "name": "billing_webhook_events_provider_event_uidx", + "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_webhook_events_queue_idx": { + "name": "billing_webhook_events_queue_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_webhook_events_status_check": { + "name": "billing_webhook_events_status_check", + "value": "\"billing_webhook_events\".\"status\" IN ('pending', 'processing', 'processed', 'ignored', 'quarantined', 'failed')" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/apps/api/drizzle/meta/_journal.json b/apps/api/drizzle/meta/_journal.json index 1cc64cbe..6ba33f99 100644 --- a/apps/api/drizzle/meta/_journal.json +++ b/apps/api/drizzle/meta/_journal.json @@ -225,6 +225,13 @@ "when": 1793851987921, "tag": "0031_email_drafts", "breakpoints": true + }, + { + "idx": 32, + "version": "7", + "when": 1794334080587, + "tag": "0032_organization_feedback_notice", + "breakpoints": true } ] } \ No newline at end of file diff --git a/apps/api/openapi.json b/apps/api/openapi.json index 16581a32..24eecaff 100644 --- a/apps/api/openapi.json +++ b/apps/api/openapi.json @@ -31205,6 +31205,64 @@ ] } }, + "/settings/feedback-requirement": { + "get": { + "description": "On SendLit Cloud, once the organization has sent `requiredAfterSends` emails, SendLit only sends through ESPs with a healthy bounce/complaint feedback connection. `required` is false on self-hosted SendLit.\n\n**Authentication:** Requires a team API key or a user OAuth access token. Dashboard session cookies are also accepted.", + "summary": "Get when ESPs need a delivery-feedback connection", + "tags": [ + "Settings" + ], + "parameters": [], + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "required": { + "type": "boolean" + }, + "requiredAfterSends": { + "type": "integer" + }, + "sentSoFar": { + "type": "integer" + } + }, + "required": [ + "required", + "requiredAfterSends", + "sentSoFar" + ] + } + } + } + }, + "400": { + "description": "`credential_ambiguous`: the request sent more than one credential (for example an API key and a bearer token). Send exactly one." + } + }, + "security": [ + { + "teamApiKey": [] + }, + { + "teamApiKeyStandardHeader": [] + }, + { + "userAccessToken": [] + }, + { + "dashboardSession": [] + }, + { + "dashboardSecureSession": [] + } + ] + } + }, "/settings/esps/{espId}/feedback": { "get": { "description": "Returns null when feedback hasn't been configured for this ESP yet.\n\n**Authentication:** Requires a team API key or a user OAuth access token. Dashboard session cookies are also accepted.", diff --git a/apps/api/src/automation/process-ongoing-sequence.test.ts b/apps/api/src/automation/process-ongoing-sequence.test.ts index d8d4a841..bd2e481d 100644 --- a/apps/api/src/automation/process-ongoing-sequence.test.ts +++ b/apps/api/src/automation/process-ongoing-sequence.test.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, type Mock, mock, vi } from "bun:test"; +import { beforeEach, describe, expect, it, type Mock, mock, spyOn, vi } from "bun:test"; import { waitFor } from "@sendlit/test-utils"; import { eq } from "drizzle-orm"; @@ -19,11 +19,15 @@ const { outboundMessages, sequenceEmails, sequences, + teamSendingControls, } = await import("../db/schema"); const { addOrStrengthenSuppression } = await import( "../delivery-feedback/suppression-queries" ); const { sendMail } = await import("../mail/send"); +const deliveryQueries = await import("../delivery/queries"); +const billingNotifications = await import("../billing/notifications"); +const { PlanGateError } = await import("../billing/errors"); const { SendDeferredError } = await import("../mail/rate-limit"); const { seedTeamAndContact, truncateAll } = await import("../test/db"); type TestDb = import("../test/db").TestDb; @@ -608,10 +612,11 @@ describe("processOngoingSequence", () => { .select() .from(ongoingSequences) .where(eq(ongoingSequences.id, row.id)); - // The ESP-existence guard throws before the retry-tracking path, so - // the row is left untouched rather than treated as a transient send - // failure — never silently retried through a different transport. + // Never retried through a different transport: it backs off and stays + // pinned, instead of failing again on every scheduler tick. expect(after.retryCount).toBe(0); + expect(after.processingStartedAt).toBeNull(); + expect(after.nextEmailScheduledTime).toBeGreaterThan(Date.now() + 4 * 60_000); }); it("cascade-deletes the ongoing_sequences row when its contact is deleted", async () => { @@ -673,6 +678,162 @@ describe("processOngoingSequence", () => { expect(after.sentEmailIds).toEqual([]); }); + it("keeps deliveries queued, without failing, while the team's sending is paused", async () => { + const { team, contact } = await seedTeamAndContact(tdb); + const { sequenceRow } = await seedSequence(tdb, { + teamId: team.id, + emails: [{ emailId: "email_e1" }], + }); + const row = await seedOngoingSequence(tdb, { + teamId: team.id, + sequenceId: sequenceRow.id, + contactId: contact.id, + }); + await tdb + .insert(teamSendingControls) + .values({ teamId: team.id, status: "marketing_paused" }); + const before = Date.now(); + + await expect(processOngoingSequence(row.id)).resolves.toBeUndefined(); + + expect(mockedSendMail).not.toHaveBeenCalled(); + const [after] = await tdb + .select() + .from(ongoingSequences) + .where(eq(ongoingSequences.id, row.id)); + expect(after.retryCount).toBe(0); + expect(after.processingStartedAt).toBeNull(); + expect(after.sentEmailIds).toEqual([]); + // Not due again on the next scheduler tick. + expect(after.nextEmailScheduledTime).toBeGreaterThanOrEqual(before + 15 * 60_000); + }); + + it("pauses and emails the owners once when sending is blocked until the team fixes it", async () => { + const { team, contact } = await seedTeamAndContact(tdb); + const { sequenceRow } = await seedSequence(tdb, { + teamId: team.id, + emails: [{ emailId: "email_e1" }], + }); + const second = await tdb + .insert(contacts) + .values({ + teamId: team.id, + email: `second-${crypto.randomUUID()}@inbox.sendlit.app`, + unsubscribeToken: crypto.randomUUID(), + }) + .returning(); + const rows = [ + await seedOngoingSequence(tdb, { + teamId: team.id, + sequenceId: sequenceRow.id, + contactId: contact.id, + }), + await seedOngoingSequence(tdb, { + teamId: team.id, + sequenceId: sequenceRow.id, + contactId: second[0].id, + }), + ]; + const resolve = spyOn( + deliveryQueries, + "resolvePinnedDeliverySource", + ).mockRejectedValue( + new PlanGateError("sending_paused", { reason: "feedback_connection_required" }), + ); + const notify = spyOn(billingNotifications, "notifyOrganizationOwners") + .mockClear() + .mockResolvedValue(undefined); + try { + await Promise.all(rows.map((row) => processOngoingSequence(row.id))); + // Picked up again after the pause: still nothing sent, nothing failed. + await processOngoingSequence(rows[0].id); + } finally { + resolve.mockRestore(); + } + + expect(mockedSendMail).not.toHaveBeenCalled(); + const [seqAfter] = await tdb + .select() + .from(sequences) + .where(eq(sequences.id, sequenceRow.id)); + expect(seqAfter.status).toBe("paused"); + expect((seqAfter.report as any).autoPause).toMatchObject({ + reason: "sending_blocked", + error: "feedback_connection_required", + }); + expect(notify).toHaveBeenCalledTimes(1); + expect(notify.mock.calls[0][2]).toContain("bounces and spam complaints"); + notify.mockRestore(); + const remaining = await tdb + .select() + .from(ongoingSequences) + .where(eq(ongoingSequences.sequenceId, sequenceRow.id)); + expect(remaining).toHaveLength(2); + expect(remaining.every((row) => row.retryCount === 0)).toBe(true); + expect(remaining.every((row) => row.processingStartedAt === null)).toBe(true); + }); + + it("pauses and emails the owners once when an error keeps failing every delivery before the send", async () => { + const { team } = await seedTeamAndContact(tdb); + const { sequenceRow } = await seedSequence(tdb, { + teamId: team.id, + emails: [{ emailId: "email_e1" }], + }); + const rows = []; + for (let i = 0; i < 6; i++) { + const [c] = await tdb + .insert(contacts) + .values({ + teamId: team.id, + email: `r${i}-${crypto.randomUUID()}@inbox.sendlit.app`, + unsubscribeToken: crypto.randomUUID(), + }) + .returning(); + rows.push( + await seedOngoingSequence(tdb, { + teamId: team.id, + sequenceId: sequenceRow.id, + contactId: c.id, + }), + ); + } + const resolve = spyOn( + deliveryQueries, + "resolvePinnedDeliverySource", + ).mockRejectedValue(new Error("delivery_source_unavailable")); + const notify = spyOn(billingNotifications, "notifyOrganizationOwners") + .mockClear() + .mockResolvedValue(undefined); + let attempts: number; + try { + for (const row of rows) { + await processOngoingSequence(row.id).catch(() => undefined); + } + // Every row is now waiting or the sequence is paused: nothing is due. + for (const row of rows) await processOngoingSequence(row.id); + attempts = resolve.mock.calls.length; + } finally { + resolve.mockRestore(); + } + + // Five failed attempts, then the pause stops the rest. + expect(attempts).toBe(5); + const [seqAfter] = await tdb + .select() + .from(sequences) + .where(eq(sequences.id, sequenceRow.id)); + expect(seqAfter.status).toBe("paused"); + expect((seqAfter.report as any).autoPause.reason).toBe("consecutive_failures"); + expect(notify).toHaveBeenCalledTimes(1); + notify.mockRestore(); + const remaining = await tdb + .select() + .from(ongoingSequences) + .where(eq(ongoingSequences.sequenceId, sequenceRow.id)); + expect(remaining).toHaveLength(6); + expect(remaining.every((row) => row.retryCount === 0)).toBe(true); + }); + it("waits for an exhausted organization quota instead of failing", async () => { const { team, contact } = await seedTeamAndContact(tdb); const { sequenceRow } = await seedSequence(tdb, { diff --git a/apps/api/src/automation/process-ongoing-sequence.ts b/apps/api/src/automation/process-ongoing-sequence.ts index 820e2c52..fd061ba3 100644 --- a/apps/api/src/automation/process-ongoing-sequence.ts +++ b/apps/api/src/automation/process-ongoing-sequence.ts @@ -2,6 +2,7 @@ import { eq, sql } from "drizzle-orm"; import { ParseError, RenderError, TokenizationError } from "liquidjs"; import { recordPacedSend } from "../abuse/telemetry"; import { commitSendReservation, releaseSendReservation } from "../billing/entitlements"; +import { isPlanGateError } from "../billing/errors"; import { sequenceBounceLimit } from "../config/constants"; import { addTagToContact, @@ -17,6 +18,7 @@ import { } from "../db/schema"; import { resolvePinnedDeliverySource } from "../delivery/queries"; import { commitQuotaForOutbound } from "../delivery/quota"; +import { notifyFeedbackMilestones } from "../delivery-feedback/feedback-requirement"; import { markOutboundAccepted } from "../delivery-feedback/outbound-queries"; import { createPinnedOutboundMessage, @@ -58,6 +60,7 @@ import { readSendingHealthConfig, recordSendFailure, recordSendSuccess, + teamFixableSendingStop, } from "./sending-health"; /** Reports (doesn't stop) a sequence's first email that went out before its @@ -100,6 +103,29 @@ function reportEarlySend( * queued for when it resumes. */ class SequencePausedError extends Error {} +/** How long to wait before checking a stopped organization or team again. */ +const SENDING_PAUSED_RECHECK_MS = 15 * 60 * 1000; + +/** Sending is stopped for the whole organization or team (an unpaid plan + * past its grace period, a reputation or abuse stop). That lifts by itself, + * not per recipient, so keep the delivery queued and check again later + * instead of failing it or retrying on every tick. Stops the team has to fix + * pause the sequence instead (see `teamFixableSendingStop`). */ +function sendingPausedDeferral(error: unknown): SendDeferredError | null { + if (!isPlanGateError(error) || error.code !== "sending_paused") return null; + return new SendDeferredError( + SENDING_PAUSED_RECHECK_MS + Math.floor(Math.random() * 300_000), + "sending_paused", + ); +} + +async function deferOngoingSequence(id: string, deferral: SendDeferredError) { + await db + .update(ongoingSequences) + .set({ nextEmailScheduledTime: Date.now() + deferral.retryAfterMs }) + .where(eq(ongoingSequences.id, id)); +} + type OngoingSequenceRow = typeof ongoingSequences.$inferSelect; type SequenceEmailRow = typeof sequenceEmails.$inferSelect; @@ -114,9 +140,11 @@ export async function processOngoingSequence(ongoingSequenceId: string) { const ongoingSequence = await claimOngoingSequence(ongoingSequenceId); if (!ongoingSequence) return; + let sequenceRow: Awaited> | undefined; + let contact: Awaited> | undefined; try { - const sequenceRow = await getSequenceRowById(ongoingSequence.sequenceId); - const contact = await getContactById(ongoingSequence.contactId); + sequenceRow = await getSequenceRowById(ongoingSequence.sequenceId); + contact = await getContactById(ongoingSequence.contactId); const team = await getTeam(ongoingSequence.teamId); if (!sequenceRow || !contact || !team) { @@ -200,6 +228,20 @@ export async function processOngoingSequence(ongoingSequenceId: string) { // Rate limited (the row was rescheduled) or the sequence was just paused: // not a job failure. if (err instanceof SequencePausedError) return; + // The stop check before the send (resolving the delivery source) runs + // outside attemptMailSending's rescheduling, so defer it here. + const pausedDeferral = sendingPausedDeferral(err); + if (pausedDeferral) { + await deferOngoingSequence(ongoingSequenceId, pausedDeferral); + logger.info( + { + ongoing_sequence_id: ongoingSequenceId, + retry_after_ms: pausedDeferral.retryAfterMs, + }, + "send deferred while sending is paused", + ); + return; + } if (err instanceof SendDeferredError) { logger.info( { ongoing_sequence_id: ongoingSequenceId, retry_after_ms: err.retryAfterMs }, @@ -207,10 +249,35 @@ export async function processOngoingSequence(ongoingSequenceId: string) { ); return; } - logger.error( - { error: err.message, ongoing_sequence_id: ongoingSequenceId }, - "processOngoingSequence failed", - ); + // A failure before the send itself (resolving the ESP, loading the + // email) counts like a failed send, so it backs off, and a run of them + // pauses the sequence and tells the owners, instead of the same error + // being retried and logged on every scheduler tick. + if (!countedFailures.has(err)) { + logger.error( + { error: err?.message, ongoing_sequence_id: ongoingSequenceId }, + "processOngoingSequence failed", + ); + try { + if (sequenceRow && contact) { + await recordFailedAttempt({ + sequence: sequenceRow, + ongoingSequence, + contactId: contact.contactId, + err, + beforeSend: true, + }); + } else { + await db + .update(ongoingSequences) + .set({ nextEmailScheduledTime: Date.now() + failedAttemptBackoffMs(1) }) + .where(eq(ongoingSequences.id, ongoingSequence.id)); + } + } catch (pauseErr) { + if (pauseErr instanceof SequencePausedError) return; + throw pauseErr; + } + } throw err; } } @@ -268,6 +335,18 @@ async function attemptMailSending({ espConfigId: sequence.outboxId, espGrantId: sequence.espGrantId, purpose: "marketing", + }).catch(async (err) => { + // A stop only the team can lift: pause and tell the owners (once, as + // only the first delivery to see it pauses the sequence), keeping + // everyone queued for when it is resumed. + const stop = teamFixableSendingStop(err); + if (!stop) throw err; + const paused = await autoPauseSequence(sequence, "sending_blocked", { + error: stop, + }); + if (paused || (await isSequencePaused(sequence.id))) + throw new SequencePausedError(); + throw err; }); const from = getEmailFrom({ name: pin.fromName, @@ -477,6 +556,7 @@ async function attemptMailSending({ }); await commitQuotaForOutbound(outbound.id); await commitSendReservation(outbound.id); + await notifyFeedbackMilestones(team.id); if ((sequence.report as { health?: unknown } | null)?.health) { await recordSendSuccess(sequence.id); } @@ -484,7 +564,9 @@ async function attemptMailSending({ } catch (err: any) { if (err instanceof SequencePausedError) throw err; const deferral = - err instanceof SendDeferredError ? err : quotaExhaustionDeferral(err); + err instanceof SendDeferredError + ? err + : (quotaExhaustionDeferral(err) ?? sendingPausedDeferral(err)); if (deferral) { if ( deferral.message === "marketing_hourly_pacing" || @@ -500,53 +582,16 @@ async function attemptMailSending({ // Try again once the ESP has capacity (rate limit, daily warm-up cap // or quota). Not a failed attempt, so the retry count (which // eventually marks the contact failed) stays put. - await db - .update(ongoingSequences) - .set({ nextEmailScheduledTime: Date.now() + deferral.retryAfterMs }) - .where(eq(ongoingSequences.id, ongoingSequence.id)); + await deferOngoingSequence(ongoingSequence.id, deferral); throw deferral; } - // Five failures in a row, or the ESP refusing outright, pause the whole - // sequence instead of failing every remaining recipient. - const failures = await recordSendFailure(sequence.id); - const fatal = isFatalEspError(err); - if (fatal || failures >= readSendingHealthConfig().consecutiveFailures) { - await autoPauseSequence(sequence, fatal ? "esp_error" : "consecutive_failures", { - error: String(err?.message ?? err).slice(0, 300), - failures, - }); - throw new SequencePausedError(); - } - const retryCount = ongoingSequence.retryCount + 1; - if (retryCount >= sequenceBounceLimit) { - if (outboundForReservation) { - await releaseSendReservation(outboundForReservation.id).catch(() => undefined); - } - await db - .update(sequences) - .set({ - report: { - ...(sequence.report as any), - sequence: { - ...(sequence.report as any)?.sequence, - failed: [ - ...((sequence.report as any)?.sequence?.failed || []), - contact.contactId, - ], - }, - }, - }) - .where(eq(sequences.id, sequence.id)); - await deleteOngoingSequence(ongoingSequence.id); - } else { - // Keep the reservation through a transient retry. It expires and - // is reopened atomically by reserveSend, preventing a retry from - // bypassing the monthly quota. - await db - .update(ongoingSequences) - .set({ retryCount }) - .where(eq(ongoingSequences.id, ongoingSequence.id)); - } + await recordFailedAttempt({ + sequence, + ongoingSequence, + contactId: contact.contactId, + err, + outboundId: outboundForReservation?.id, + }); logger.error( { error: err.message, @@ -555,10 +600,82 @@ async function attemptMailSending({ }, "attemptMailSending failed", ); + if (err && typeof err === "object") countedFailures.add(err); throw err; } } +/** Errors already counted by `recordFailedAttempt`, so the caller doesn't + * count them twice. */ +const countedFailures = new WeakSet(); + +/** Wait before retrying a failed attempt: 5 minutes, doubling, at most an + * hour, so a persistent error isn't retried on every scheduler tick. */ +export function failedAttemptBackoffMs(retryCount: number): number { + return Math.min(5 * 60_000 * 2 ** Math.max(0, retryCount - 1), 60 * 60_000); +} + +/** + * Counts a failed attempt to send a sequence's email. Five in a row, or the + * ESP refusing outright, pause the whole sequence and tell the owners + * (throws `SequencePausedError`) instead of failing every remaining + * recipient. Otherwise the delivery is retried after a backoff. A failed + * send uses up one of the recipient's retries (marking them failed once + * they run out); a failure before the send (`beforeSend`: loading data, + * resolving the ESP) isn't theirs, so it only waits. + */ +async function recordFailedAttempt({ + sequence, + ongoingSequence, + contactId, + err, + outboundId, + beforeSend = false, +}: { + sequence: typeof sequences.$inferSelect; + ongoingSequence: OngoingSequenceRow; + contactId: string; + err: any; + outboundId?: string; + beforeSend?: boolean; +}): Promise { + const failures = await recordSendFailure(sequence.id); + const fatal = isFatalEspError(err); + if (fatal || failures >= readSendingHealthConfig().consecutiveFailures) { + await autoPauseSequence(sequence, fatal ? "esp_error" : "consecutive_failures", { + error: String(err?.message ?? err).slice(0, 300), + failures, + }); + throw new SequencePausedError(); + } + if (beforeSend) { + await db + .update(ongoingSequences) + .set({ nextEmailScheduledTime: Date.now() + failedAttemptBackoffMs(1) }) + .where(eq(ongoingSequences.id, ongoingSequence.id)); + return; + } + const retryCount = ongoingSequence.retryCount + 1; + if (retryCount >= sequenceBounceLimit) { + if (outboundId) { + await releaseSendReservation(outboundId).catch(() => undefined); + } + await recordRecipientFailed(sequence.id, contactId); + await deleteOngoingSequence(ongoingSequence.id); + return; + } + // Keep any send reservation through a transient retry. It expires and is + // reopened atomically by reserveSend, preventing a retry from bypassing + // the monthly quota. + await db + .update(ongoingSequences) + .set({ + retryCount, + nextEmailScheduledTime: Date.now() + failedAttemptBackoffMs(retryCount), + }) + .where(eq(ongoingSequences.id, ongoingSequence.id)); +} + /** Whether a render error comes from the email's content or the team's footer * settings, and so repeats for every recipient until the team changes them. */ export function isPermanentRenderError(err: unknown): boolean { diff --git a/apps/api/src/automation/sending-health.test.ts b/apps/api/src/automation/sending-health.test.ts index 464f0f76..36af28ed 100644 --- a/apps/api/src/automation/sending-health.test.ts +++ b/apps/api/src/automation/sending-health.test.ts @@ -170,7 +170,7 @@ describe("auto-pause (#25)", () => { expect((await sequenceById(sequenceRow.id)).status).toBe("completed"); }); - it("retries a transient error while rendering without pausing or counting it", async () => { + it("retries a transient error while rendering later, without pausing or using up a retry", async () => { const { sequenceRow, rows } = await broadcastTo(1); const spy = spyOn(generalSettings, "getTeamSettingsRow").mockRejectedValueOnce( new Error("Connection terminated unexpectedly"), @@ -187,7 +187,13 @@ describe("auto-pause (#25)", () => { const [row] = await tdb.select().from(schema.ongoingSequences); expect(row.retryCount).toBe(0); expect(row.processingStartedAt).toBeNull(); + // Waits instead of being retried on every scheduler tick. + expect(row.nextEmailScheduledTime).toBeGreaterThan(Date.now() + 4 * 60_000); // Recovered: the next attempt sends normally. + await tdb + .update(schema.ongoingSequences) + .set({ nextEmailScheduledTime: Date.now() - 1 }) + .where(eq(schema.ongoingSequences.id, row.id)); await processOngoingSequence(rows[0].id); expect(mockedSendMail).toHaveBeenCalledTimes(1); }); diff --git a/apps/api/src/automation/sending-health.ts b/apps/api/src/automation/sending-health.ts index 39159dda..2f23d0d9 100644 --- a/apps/api/src/automation/sending-health.ts +++ b/apps/api/src/automation/sending-health.ts @@ -1,4 +1,5 @@ import { and, count, eq, gte, isNotNull, sql } from "drizzle-orm"; +import { isPlanGateError } from "../billing/errors"; import { notifyOrganizationOwners } from "../billing/notifications"; import { positiveInt } from "../billing/reputation-config"; import { db } from "../db/client"; @@ -26,7 +27,8 @@ export type AutoPauseReason = | "complaint_rate" | "consecutive_failures" | "esp_error" - | "render_error"; + | "render_error" + | "sending_blocked"; export type SequenceDeliveryRates = { sent: number; @@ -114,6 +116,47 @@ export function renderErrorReason(code: string): string { } } +/** Organization-wide sending stops only the team can lift (see + * `assertSendingEligibility` in billing/domains.ts). Sends hitting one pause + * the sequence and tell the owners, instead of waiting for a fix nobody + * knows is needed. Other stops (billing, abuse review) lift by themselves or + * notify on their own, so their sends just wait. */ +const TEAM_FIXABLE_SENDING_STOPS = new Set([ + "feedback_connection_required", + "verified_owner_email_required", +]); + +/** The team-fixable stop behind a `sending_paused` error, if it is one. */ +export function teamFixableSendingStop(error: unknown): string | null { + if (!isPlanGateError(error) || error.code !== "sending_paused") return null; + const reason = error.details.reason; + return typeof reason === "string" && TEAM_FIXABLE_SENDING_STOPS.has(reason) + ? reason + : null; +} + +/** Why a `sending_blocked` auto-pause happened and what to do, in words for + * the team. */ +export function sendingBlockedReason(code: string): { why: string; next: string } { + switch (code) { + case "feedback_connection_required": + return { + why: "its email provider isn't reporting bounces and spam complaints back to SendLit, which is required after your first 100 emails", + next: "Connect bounce and complaint feedback for the provider in Settings → Email service providers (ESP)", + }; + case "verified_owner_email_required": + return { + why: "the organization's owner hasn't verified their email address", + next: "Verify the owner's email address", + }; + default: + return { + why: `sending is blocked for the organization (${code})`, + next: "Check your organization's settings", + }; + } +} + export async function autoPauseSequence( sequence: Pick< typeof sequences.$inferSelect, @@ -160,11 +203,15 @@ export async function autoPauseSequence( ? "several emails in a row failed to send" : reason === "render_error" ? renderErrorReason(String(details.error ?? "")) - : `your email provider refused to send (${String(details.error ?? "")})`; + : reason === "sending_blocked" + ? sendingBlockedReason(String(details.error ?? "")).why + : `your email provider refused to send (${String(details.error ?? "")})`; const next = reason === "render_error" ? "Check your mailing address in Settings and the email's content, then resume it from SendLit" - : "Sending more would hurt your sender reputation. Check your list and your email provider, then resume it from SendLit"; + : reason === "sending_blocked" + ? `${sendingBlockedReason(String(details.error ?? "")).next}, then resume it from SendLit` + : "Sending more would hurt your sender reputation. Check your list and your email provider, then resume it from SendLit"; await notifyOrganizationOwners( team.organizationId, `Paused: "${sequence.title}"`, diff --git a/apps/api/src/billing/domains.ts b/apps/api/src/billing/domains.ts index 878a4206..d40df44a 100644 --- a/apps/api/src/billing/domains.ts +++ b/apps/api/src/billing/domains.ts @@ -232,6 +232,39 @@ export async function revokeSendingDomain( return Boolean(updated); } +/** How many emails an organization can send on SendLit Cloud before each + * ESP it sends through needs a healthy bounce/complaint feedback connection. */ +export function testVolumeThreshold(): number { + const thresholdRaw = process.env.BILLING_TEST_VOLUME_THRESHOLD; + const threshold = + thresholdRaw === undefined || thresholdRaw === "" ? 100 : Number(thresholdRaw); + if (!Number.isSafeInteger(threshold) || threshold <= 0) + throw new Error("BILLING_TEST_VOLUME_THRESHOLD_invalid"); + return threshold; +} + +/** Whether the feedback requirement applies (cloud only). */ +export function feedbackRequirementApplies(): boolean { + return process.env.SENDLIT_DEPLOYMENT_MODE === "cloud"; +} + +/** Emails the organization's ESPs have accepted, across all its teams. */ +export async function organizationAcceptedSends( + organizationId: string, +): Promise { + const [usage] = await db + .select({ value: count() }) + .from(outboundMessages) + .innerJoin(teams, eq(teams.id, outboundMessages.teamId)) + .where( + and( + eq(teams.organizationId, organizationId), + gte(outboundMessages.acceptedAt, new Date(0)), + ), + ); + return Number(usage?.value ?? 0); +} + /** * Cloud test volume is intentionally small, but once crossed we require an * exact verified From domain. This check is performed at the delivery-source @@ -243,7 +276,7 @@ export async function assertSendingEligibility( teamId?: string, espConfigId?: string, ): Promise { - if (process.env.SENDLIT_DEPLOYMENT_MODE !== "cloud") return; + if (!feedbackRequirementApplies()) return; const [owner] = await db .select({ id: organizationMembers.id }) .from(organizationMembers) @@ -262,22 +295,7 @@ export async function assertSendingEligibility( reason: "verified_owner_email_required", }); - const thresholdRaw = process.env.BILLING_TEST_VOLUME_THRESHOLD; - const threshold = - thresholdRaw === undefined || thresholdRaw === "" ? 100 : Number(thresholdRaw); - if (!Number.isSafeInteger(threshold) || threshold <= 0) - throw new Error("BILLING_TEST_VOLUME_THRESHOLD_invalid"); - const [usage] = await db - .select({ value: count() }) - .from(outboundMessages) - .innerJoin(teams, eq(teams.id, outboundMessages.teamId)) - .where( - and( - eq(teams.organizationId, organizationId), - gte(outboundMessages.acceptedAt, new Date(0)), - ), - ); - if (Number(usage?.value ?? 0) < threshold) return; + if ((await organizationAcceptedSends(organizationId)) < testVolumeThreshold()) return; if (teamId && espConfigId) { const [feedback] = await db diff --git a/apps/api/src/db/schema-core.ts b/apps/api/src/db/schema-core.ts index 2b8b9836..b208e60b 100644 --- a/apps/api/src/db/schema-core.ts +++ b/apps/api/src/db/schema-core.ts @@ -67,6 +67,10 @@ export const organizations = pgTable( /** Name of the first team, created once a `pending_payment` * organization's checkout is paid. */ pendingTeamName: text("pending_team_name"), + /** The last warning (50, 90 or 100 percent of the cloud test volume) + * emailed to the owners about connecting bounce/complaint feedback, so + * each goes out once. */ + feedbackNoticePercent: integer("feedback_notice_percent").notNull().default(0), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), }, diff --git a/apps/api/src/delivery-feedback/feedback-requirement.test.ts b/apps/api/src/delivery-feedback/feedback-requirement.test.ts new file mode 100644 index 00000000..84a3cb7c --- /dev/null +++ b/apps/api/src/delivery-feedback/feedback-requirement.test.ts @@ -0,0 +1,150 @@ +import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test"; +import { eq } from "drizzle-orm"; + +mock.module("../db/client", async () => { + const { makeTestDb } = await import("../test/db.js"); + return { db: await makeTestDb() }; +}); + +const { db } = await import("../db/client"); +const schema = await import("../db/schema"); +const { seedTeamAndContact, truncateAll } = await import("../test/db"); +type TestDb = import("../test/db").TestDb; +const notifications = await import("../billing/notifications"); +const { encryptSecret } = await import("../utils/secret-crypto"); +const { getFeedbackRequirement, notifyFeedbackMilestones } = await import( + "./feedback-requirement" +); + +const tdb = db as unknown as TestDb; +const previousMode = process.env.SENDLIT_DEPLOYMENT_MODE; +const previousThreshold = process.env.BILLING_TEST_VOLUME_THRESHOLD; + +beforeEach(async () => { + await truncateAll(tdb); + process.env.SENDLIT_DEPLOYMENT_MODE = "cloud"; + process.env.BILLING_TEST_VOLUME_THRESHOLD = "10"; +}); + +afterEach(() => { + process.env.SENDLIT_DEPLOYMENT_MODE = previousMode; + process.env.BILLING_TEST_VOLUME_THRESHOLD = previousThreshold; + if (previousMode === undefined) delete process.env.SENDLIT_DEPLOYMENT_MODE; + if (previousThreshold === undefined) delete process.env.BILLING_TEST_VOLUME_THRESHOLD; +}); + +async function setup() { + const { organization, team } = await seedTeamAndContact(tdb); + const [esp] = await tdb + .select() + .from(schema.espConfigs) + .where(eq(schema.espConfigs.teamId, team.id)); + return { organization, team, esp }; +} + +async function accept(teamId: string, espConfigId: string, count: number) { + for (let i = 0; i < count; i++) { + const recipient = `r${crypto.randomUUID()}@example.com`; + await tdb.insert(schema.outboundMessages).values({ + teamId, + deliverySourceType: "team", + espConfigId, + sourceType: "campaign", + recipientEmail: recipient, + normalizedRecipient: recipient, + deliveryStatus: "accepted", + acceptedAt: new Date(), + }); + } +} + +describe("feedback requirement", () => { + it("reports the cloud requirement and how far the organization is", async () => { + const { organization, team, esp } = await setup(); + await accept(team.id, esp.id, 3); + + expect(await getFeedbackRequirement(organization.id)).toEqual({ + required: true, + requiredAfterSends: 10, + sentSoFar: 3, + }); + process.env.SENDLIT_DEPLOYMENT_MODE = "oss"; + expect((await getFeedbackRequirement(organization.id)).required).toBe(false); + }); + + it("emails the owners once each at 50%, 90% and 100% while feedback isn't connected", async () => { + const { organization, team, esp } = await setup(); + const notify = spyOn(notifications, "notifyOrganizationOwners").mockResolvedValue( + undefined, + ); + try { + await accept(team.id, esp.id, 4); + await notifyFeedbackMilestones(team.id); + expect(notify).not.toHaveBeenCalled(); + + await accept(team.id, esp.id, 1); + await notifyFeedbackMilestones(team.id); + await notifyFeedbackMilestones(team.id); + expect(notify).toHaveBeenCalledTimes(1); + expect(notify.mock.calls[0][0]).toBe(organization.id); + expect(notify.mock.calls[0][1]).toContain("5 of 10"); + + await accept(team.id, esp.id, 4); + await notifyFeedbackMilestones(team.id); + expect(notify).toHaveBeenCalledTimes(2); + expect(notify.mock.calls[1][1]).toContain("9 of 10"); + + await accept(team.id, esp.id, 1); + await Promise.all([ + notifyFeedbackMilestones(team.id), + notifyFeedbackMilestones(team.id), + ]); + expect(notify).toHaveBeenCalledTimes(3); + expect(notify.mock.calls[2][1]).toContain("Sending stopped"); + expect(notify.mock.calls[2][2]).toContain(esp.name); + + await accept(team.id, esp.id, 5); + await notifyFeedbackMilestones(team.id); + expect(notify).toHaveBeenCalledTimes(3); + } finally { + notify.mockRestore(); + } + }); + + it("doesn't email when every ESP already has healthy feedback", async () => { + const { team, esp } = await setup(); + await tdb.insert(schema.espFeedbackConnections).values({ + teamId: team.id, + espConfigId: esp.id, + ownerScope: "team", + provider: "postmark", + encryptedCredentials: encryptSecret("webhook-secret"), + status: "healthy", + }); + const notify = spyOn(notifications, "notifyOrganizationOwners").mockResolvedValue( + undefined, + ); + try { + await accept(team.id, esp.id, 10); + await notifyFeedbackMilestones(team.id); + expect(notify).not.toHaveBeenCalled(); + } finally { + notify.mockRestore(); + } + }); + + it("does nothing on self-hosted SendLit", async () => { + process.env.SENDLIT_DEPLOYMENT_MODE = "oss"; + const { team, esp } = await setup(); + const notify = spyOn(notifications, "notifyOrganizationOwners").mockResolvedValue( + undefined, + ); + try { + await accept(team.id, esp.id, 10); + await notifyFeedbackMilestones(team.id); + expect(notify).not.toHaveBeenCalled(); + } finally { + notify.mockRestore(); + } + }); +}); diff --git a/apps/api/src/delivery-feedback/feedback-requirement.ts b/apps/api/src/delivery-feedback/feedback-requirement.ts new file mode 100644 index 00000000..45ee4bc0 --- /dev/null +++ b/apps/api/src/delivery-feedback/feedback-requirement.ts @@ -0,0 +1,158 @@ +import { and, eq, inArray, lt } from "drizzle-orm"; +import { + feedbackRequirementApplies, + organizationAcceptedSends, + testVolumeThreshold, +} from "../billing/domains"; +import { notifyOrganizationOwners } from "../billing/notifications"; +import { db } from "../db/client"; +import { espConfigs, espFeedbackConnections, organizations, teams } from "../db/schema"; +import { captureError, captureEvent } from "../observability"; +import logger from "../services/log"; + +/** Warnings, in percent of the cloud test volume, emailed to the owners + * while an ESP still has no feedback connection. */ +const NOTICE_PERCENTS = [50, 90, 100] as const; + +export type FeedbackRequirement = { + /** Whether ESPs need a healthy bounce/complaint feedback connection once + * the organization has sent `requiredAfterSends` emails (SendLit Cloud). */ + required: boolean; + requiredAfterSends: number; + /** Emails the organization has sent so far, across all its teams. */ + sentSoFar: number; +}; + +export async function getFeedbackRequirement( + organizationId: string, +): Promise { + const required = feedbackRequirementApplies(); + return { + required, + requiredAfterSends: testVolumeThreshold(), + sentSoFar: required ? await organizationAcceptedSends(organizationId) : 0, + }; +} + +/** The organization's active team ESPs without a healthy feedback + * connection: the ones `assertSendingEligibility` stops past the test + * volume. */ +async function espsWithoutFeedback(organizationId: string) { + const esps = await db + .select({ id: espConfigs.id, teamId: espConfigs.teamId, name: espConfigs.name }) + .from(espConfigs) + .innerJoin(teams, eq(teams.id, espConfigs.teamId)) + .where( + and(eq(teams.organizationId, organizationId), eq(espConfigs.status, "active")), + ); + if (esps.length === 0) return []; + const healthy = await db + .select({ + teamId: espFeedbackConnections.teamId, + espConfigId: espFeedbackConnections.espConfigId, + }) + .from(espFeedbackConnections) + .where( + and( + inArray( + espFeedbackConnections.espConfigId, + esps.map((esp) => esp.id), + ), + eq(espFeedbackConnections.status, "healthy"), + ), + ); + return esps.filter( + (esp) => + !healthy.some((row) => row.espConfigId === esp.id && row.teamId === esp.teamId), + ); +} + +function noticeText( + percent: (typeof NOTICE_PERCENTS)[number], + organizationName: string, + sent: number, + threshold: number, + espNames: string[], +): { subject: string; text: string } { + const esps = espNames.map((name) => `"${name}"`).join(", "); + const how = + "Connect bounce and complaint feedback for it in Settings → Email service providers (ESP), using the feedback button next to the provider."; + if (percent === 100) { + return { + subject: `Sending stopped for ${organizationName}: connect bounce and complaint feedback`, + text: `${organizationName} has sent ${sent} emails, so SendLit now needs your email provider to report bounces and spam complaints back before it sends more. Sending through ${esps} is stopped until it does; running sequences and broadcasts are paused and keep their queued emails. ${how} Then resume them.`, + }; + } + return { + subject: `${organizationName} has sent ${sent} of ${threshold} emails: connect bounce and complaint feedback`, + text: `${organizationName} has sent ${sent} of its first ${threshold} emails. After ${threshold}, SendLit only sends through an email provider that reports bounces and spam complaints back, so it can protect your sender reputation, and ${esps} doesn't yet. ${how} Without it, sending through it stops after ${threshold} emails.`, + }; +} + +/** + * Emails the organization's owners when it reaches 50%, 90% and 100% of the + * cloud test volume while one of its ESPs has no feedback connection, so + * they can connect it before sending stops. Each warning goes out once; it + * runs after every accepted send, and costs nothing once all three are past. + * Never throws: the email it follows has already gone out. + */ +export async function notifyFeedbackMilestones(teamId: string): Promise { + if (!feedbackRequirementApplies()) return; + try { + const [org] = await db + .select({ + id: organizations.id, + name: organizations.name, + noticePercent: organizations.feedbackNoticePercent, + }) + .from(teams) + .innerJoin(organizations, eq(organizations.id, teams.organizationId)) + .where(eq(teams.id, teamId)) + .limit(1); + if (!org || org.noticePercent >= 100) return; + const threshold = testVolumeThreshold(); + const sent = await organizationAcceptedSends(org.id); + const reached = NOTICE_PERCENTS.filter( + (percent) => sent * 100 >= percent * threshold, + ).at(-1); + if (!reached || reached <= org.noticePercent) return; + // Claim the warning first, so concurrent sends email it only once. + const [claimed] = await db + .update(organizations) + .set({ feedbackNoticePercent: reached }) + .where( + and( + eq(organizations.id, org.id), + lt(organizations.feedbackNoticePercent, reached), + ), + ) + .returning({ id: organizations.id }); + if (!claimed) return; + const missing = await espsWithoutFeedback(org.id); + if (missing.length === 0) return; + const { subject, text } = noticeText( + reached, + org.name, + sent, + threshold, + missing.map((esp) => esp.name), + ); + await notifyOrganizationOwners(org.id, subject, text); + logger.info( + { organization_id: org.id, notice_percent: reached }, + "feedback connection reminder sent", + ); + captureEvent({ + event: "feedback_connection_reminder_sent", + source: "delivery_feedback.requirement", + teamId, + properties: { notice_percent: reached, esp_count: missing.length }, + }); + } catch (error) { + captureError({ + error, + source: "delivery_feedback.requirement", + teamId, + }); + } +} diff --git a/apps/api/src/delivery-feedback/feedback-routes.ts b/apps/api/src/delivery-feedback/feedback-routes.ts index 0c76f81a..0767eff6 100644 --- a/apps/api/src/delivery-feedback/feedback-routes.ts +++ b/apps/api/src/delivery-feedback/feedback-routes.ts @@ -6,6 +6,7 @@ import { requireTeam } from "../auth/require-team"; import { feedbackCapableProviders } from "../config/constants"; import { captureEvent } from "../observability"; import { getEspConfigByEspId } from "../settings/esp/queries"; +import { getTeam } from "../team/queries"; import { disableFeedbackConnection, feedbackConnectionReadinessError, @@ -17,13 +18,20 @@ import { feedbackConnectionInputError, toPublicFeedbackConnection, } from "./feedback-connection-shape"; +import { getFeedbackRequirement } from "./feedback-requirement"; const router = Router(); router.use("/settings/esps/:espId/feedback", requireAuth, requireTeam); +router.use("/settings/feedback-requirement", requireAuth, requireTeam); const s = initServer(); const impl = s.router(contract.feedback, { + requirement: async ({ req }) => { + const team = await getTeam((req as any).teamId); + if (!team) throw new Error("team_not_found"); + return { status: 200, body: await getFeedbackRequirement(team.organizationId) }; + }, get: async ({ params, req }) => { const teamId = (req as any).teamId; const esp = await getEspConfigByEspId(teamId, params.espId); diff --git a/apps/api/src/mail/worker.ts b/apps/api/src/mail/worker.ts index bae20e0c..fefd9b76 100644 --- a/apps/api/src/mail/worker.ts +++ b/apps/api/src/mail/worker.ts @@ -8,6 +8,7 @@ import { evaluateTeamReputation } from "../billing/reputation"; import { db } from "../db/client"; import { resolvePinnedDeliverySource } from "../delivery/queries"; import { commitQuotaForOutbound, releaseQuotaForOutbound } from "../delivery/quota"; +import { notifyFeedbackMilestones } from "../delivery-feedback/feedback-requirement"; import { getOutboundMessageByTransactionalEmailId, markOutboundAccepted, @@ -201,6 +202,7 @@ async function processTransactionalJob(job: Job, token?: string) { }); await commitQuotaForOutbound(outbound.id); await commitSendReservation(outbound.id); + await notifyFeedbackMilestones(row.teamId); } } catch (err: any) { if (err instanceof SendDeferredError && token) { diff --git a/apps/api/src/mcp/policy.ts b/apps/api/src/mcp/policy.ts index 49dd6dbe..b6680ade 100644 --- a/apps/api/src/mcp/policy.ts +++ b/apps/api/src/mcp/policy.ts @@ -147,6 +147,7 @@ const operationScopes = { "teams.listKeys": MCP_SCOPES.apiKeysRead, "teams.createKey": MCP_SCOPES.apiKeysWrite, "teams.removeKey": MCP_SCOPES.apiKeysWrite, + "feedback.requirement": MCP_SCOPES.feedbackRead, "feedback.get": MCP_SCOPES.feedbackRead, "feedback.upsert": MCP_SCOPES.feedbackWrite, "feedback.test": MCP_SCOPES.feedbackWrite, diff --git a/apps/api/src/mcp/tools/delivery-feedback.test.ts b/apps/api/src/mcp/tools/delivery-feedback.test.ts index 74e2d548..e91ba4c8 100644 --- a/apps/api/src/mcp/tools/delivery-feedback.test.ts +++ b/apps/api/src/mcp/tools/delivery-feedback.test.ts @@ -95,6 +95,7 @@ beforeEach(() => { describe("MCP delivery-feedback tools", () => { it("serves every feedback, delivery-event and suppression tool, each with a title", () => { expect([...tools.keys()]).toEqual([ + "get_feedback_requirement", "get_esp_feedback_connection", "upsert_esp_feedback_connection", "test_esp_feedback_connection", diff --git a/apps/api/src/mcp/tools/delivery-feedback.ts b/apps/api/src/mcp/tools/delivery-feedback.ts index 62bbe4fb..67c4efad 100644 --- a/apps/api/src/mcp/tools/delivery-feedback.ts +++ b/apps/api/src/mcp/tools/delivery-feedback.ts @@ -1,3 +1,4 @@ +import { feedbackRequirementSchema } from "@sendlit/api-contract"; import { z } from "zod"; import { feedbackCapableProviders } from "../../config/constants"; import { @@ -17,6 +18,7 @@ import { feedbackConnectionInputError, toPublicFeedbackConnection, } from "../../delivery-feedback/feedback-connection-shape"; +import { getFeedbackRequirement } from "../../delivery-feedback/feedback-requirement"; import { getOutboundMessagesByIds } from "../../delivery-feedback/outbound-queries"; import { addOrStrengthenSuppression, @@ -27,6 +29,7 @@ import { type Suppression, } from "../../delivery-feedback/suppression-queries"; import { getEspConfigByEspId, listEspConfigs } from "../../settings/esp/queries"; +import { getTeam } from "../../team/queries"; import type { SendLitMcpTool } from "../context"; import { dedicatedTool } from "../tool"; import { notFound, toolError } from "./responses"; @@ -100,6 +103,23 @@ async function toPublicEvents(teamId: string, events: DeliveryEvent[]) { * (`feedbackCapableProviders`) can be configured. */ export const deliveryFeedbackTools = (): SendLitMcpTool[] => [ + dedicatedTool({ + name: "get_feedback_requirement", + operationId: "feedback.requirement", + title: "Get Feedback Requirement", + risk: "read", + description: + "Whether ESPs need a healthy bounce/complaint feedback connection to keep sending (SendLit Cloud, once the organization has sent `requiredAfterSends` emails), and how many emails it has sent so far. `required` is false on self-hosted SendLit.", + inputSchema: z.object({}), + outputSchema: feedbackRequirementSchema, + annotations: { idempotentHint: true, openWorldHint: false }, + async handler({ context }) { + const team = await getTeam(context.teamId); + if (!team) return notFound(); + return getFeedbackRequirement(team.organizationId); + }, + }), + dedicatedTool({ name: "get_esp_feedback_connection", operationId: "feedback.get", diff --git a/apps/api/src/observability/instance.ts b/apps/api/src/observability/instance.ts index 17db8370..71acf3ed 100644 --- a/apps/api/src/observability/instance.ts +++ b/apps/api/src/observability/instance.ts @@ -66,6 +66,9 @@ const PROPERTY_ALLOWLIST = new Set([ "reason", "outcome", "scan_mode", + // Feedback connection reminders (delivery-feedback/feedback-requirement.ts). + "notice_percent", + "esp_count", // Custom events (docs/custom_events.md#observability). Event names and // properties are customer-defined and never sent. "duplicate", diff --git a/apps/web/app/(dashboard)/settings/page.tsx b/apps/web/app/(dashboard)/settings/page.tsx index 0e67449f..5e957161 100644 --- a/apps/web/app/(dashboard)/settings/page.tsx +++ b/apps/web/app/(dashboard)/settings/page.tsx @@ -22,6 +22,7 @@ import { DeleteConfirmationDialog } from "@/components/dashboard/delete-confirma import { DomainAuthenticationDialog } from "@/components/dashboard/domain-authentication-dialog"; import { EspConfigurationDialog } from "@/components/dashboard/esp-configuration-dialog"; import { EspFeedbackDialog } from "@/components/dashboard/esp-feedback-dialog"; +import { FeedbackRequirementNotice } from "@/components/dashboard/feedback-requirement-notice"; import { Loading } from "@/components/dashboard/loading"; import { PageHeader } from "@/components/dashboard/page-header"; import { ScrollablePage } from "@/components/dashboard/scrollable-page"; @@ -68,8 +69,10 @@ import { deleteEsp, deleteTeamKey, type EspConfig, + type FeedbackRequirement, feedbackCapableProviders, type GeneralSettings, + getFeedbackRequirement, getGeneralSettings, getTeamDeliverySettings, listEsps, @@ -106,6 +109,8 @@ export default function SettingsPage() { const [mailingAddress, setMailingAddress] = useState(""); const [esps, setEsps] = useState(null); const [defaultTeamEspId, setDefaultTeamEspId] = useState(null); + const [feedbackRequirement, setFeedbackRequirement] = + useState(null); const [error, setError] = useState(null); const [savingGeneral, setSavingGeneral] = useState(false); const [generalSaved, setGeneralSaved] = useState(false); @@ -129,6 +134,10 @@ export default function SettingsPage() { const [apiKeyPendingDelete, setApiKeyPendingDelete] = useState(null); async function load() { + // Informational only: the page works without it. + getFeedbackRequirement() + .then(setFeedbackRequirement) + .catch(() => setFeedbackRequirement(null)); try { const [general, { items }, deliverySettings] = await Promise.all([ getGeneralSettings(), @@ -561,6 +570,10 @@ export default function SettingsPage() { require one. )} +

Configure one or more sending identities. One is the team's @@ -776,6 +789,7 @@ export default function SettingsPage() { open={espFormOpen} onOpenChange={setEspFormOpen} esp={editingEsp} + feedbackRequirement={feedbackRequirement} onSubmit={async (input) => { if (editingEsp) { await updateEsp(editingEsp.espId, input); diff --git a/apps/web/components/dashboard/auto-pause-banner.tsx b/apps/web/components/dashboard/auto-pause-banner.tsx index 81aa2d01..c00caf6d 100644 --- a/apps/web/components/dashboard/auto-pause-banner.tsx +++ b/apps/web/components/dashboard/auto-pause-banner.tsx @@ -23,6 +23,27 @@ function renderErrorReason(code: string): string { } } +/** Mirrors `sendingBlockedReason` in the API's automation/sending-health.ts. */ +function sendingBlockedReason(code: string): { why: string; next: string } { + switch (code) { + case "feedback_connection_required": + return { + why: "Your email provider isn't reporting bounces and spam complaints back to SendLit, which is required after your first 100 emails.", + next: "Connect bounce and complaint feedback for the provider in Settings → Email service providers (ESP)", + }; + case "verified_owner_email_required": + return { + why: "The organization's owner hasn't verified their email address.", + next: "Verify the owner's email address", + }; + default: + return { + why: `Sending is blocked for the organization (${code}).`, + next: "Check your organization's settings", + }; + } +} + /** Why SendLit paused a running broadcast or sequence (#25), with a way to * resume once the team has dealt with it. */ export function AutoPauseBanner({ @@ -45,11 +66,15 @@ export function AutoPauseBanner({ ? "Several emails in a row failed to send." : pause.reason === "render_error" ? renderErrorReason(pause.error ?? "") - : `Your email provider refused to send: ${pause.error ?? "unknown error"}.`; + : pause.reason === "sending_blocked" + ? sendingBlockedReason(pause.error ?? "").why + : `Your email provider refused to send: ${pause.error ?? "unknown error"}.`; const next = pause.reason === "render_error" ? "Check your mailing address in Settings and the email's content" - : "Clean your list or fix your email provider"; + : pause.reason === "sending_blocked" + ? sendingBlockedReason(pause.error ?? "").next + : "Clean your list or fix your email provider"; return ( diff --git a/apps/web/components/dashboard/esp-configuration-dialog.tsx b/apps/web/components/dashboard/esp-configuration-dialog.tsx index 2048be59..e7e45613 100644 --- a/apps/web/components/dashboard/esp-configuration-dialog.tsx +++ b/apps/web/components/dashboard/esp-configuration-dialog.tsx @@ -7,6 +7,7 @@ import { } from "@sendlit/api-contract"; import { useEffect, useState } from "react"; import { Banner } from "@/components/dashboard/banner"; +import { FeedbackRequirementNotice } from "@/components/dashboard/feedback-requirement-notice"; import { Button } from "@/components/ui/codelit/button"; import { Dialog, @@ -25,7 +26,12 @@ import { SelectValue, } from "@/components/ui/codelit/select"; import { Switch } from "@/components/ui/codelit/switch"; -import type { EspConfig, EspConnectionInput, EspProvider } from "@/lib/api"; +import type { + EspConfig, + EspConnectionInput, + EspProvider, + FeedbackRequirement, +} from "@/lib/api"; import { ApiError } from "@/lib/api-client"; import { ESP_PROVIDERS } from "@/lib/esp-providers"; import { portForTls, tlsPortMismatch } from "@/lib/smtp"; @@ -94,6 +100,7 @@ export function EspConfigurationDialog({ createLabel = "Create ESP", saveLabel = "Save ESP", namePlaceholder = "e.g. Marketing SMTP", + feedbackRequirement, }: { open: boolean; esp: EspConfig | null; @@ -104,6 +111,8 @@ export function EspConfigurationDialog({ createLabel?: string; saveLabel?: string; namePlaceholder?: string; + /** Explains, while creating an ESP, when it will need delivery feedback. */ + feedbackRequirement?: FeedbackRequirement | null; }) { const [form, setForm] = useState(emptyForm); const providerDefaultRate = defaultMaxSendRates[form.provider]; @@ -202,6 +211,7 @@ export function EspConfigurationDialog({ {esp ? (editTitle?.(esp) ?? `Edit ${esp.name}`) : createTitle} + {!esp && } {error && {error}}

diff --git a/apps/web/components/dashboard/feedback-requirement-notice.tsx b/apps/web/components/dashboard/feedback-requirement-notice.tsx new file mode 100644 index 00000000..4ec85be1 --- /dev/null +++ b/apps/web/components/dashboard/feedback-requirement-notice.tsx @@ -0,0 +1,38 @@ +import { Banner } from "@/components/dashboard/banner"; +import type { FeedbackRequirement } from "@/lib/api"; + +/** Explains SendLit Cloud's rule that each ESP needs bounce and complaint + * feedback once the organization has sent its first emails, with how close + * it is. Owners are also emailed at 50%, 90% and 100% (see the API's + * delivery-feedback/feedback-requirement.ts). Renders nothing when the rule + * doesn't apply. */ +export function FeedbackRequirementNotice({ + requirement, + newEsp = false, + className, +}: { + requirement: FeedbackRequirement | null | undefined; + /** Shown while creating an ESP, before feedback can be set up for it. */ + newEsp?: boolean; + className?: string; +}) { + if (!requirement?.required) return null; + const { requiredAfterSends: limit, sentSoFar: sent } = requirement; + const reached = sent >= limit; + const progress = reached + ? `Your organization has sent ${sent} emails, so this applies now.` + : `Your organization has sent ${sent} of them so far.`; + return ( + = limit ? "warning" : "info"} + className={className} + > + After your organization's first {limit} emails, SendLit only sends through + providers that report bounces and spam complaints back, to protect your sender + reputation. {progress}{" "} + {newEsp + ? "Once you've saved this ESP, set up its delivery feedback from the ESP list." + : "Set up delivery feedback for each ESP with its feedback button below."} + + ); +} diff --git a/apps/web/lib/api.ts b/apps/web/lib/api.ts index 4ad5628d..4be34f6b 100644 --- a/apps/web/lib/api.ts +++ b/apps/web/lib/api.ts @@ -1008,6 +1008,18 @@ export interface FeedbackConnectionInput { expectedTopicArn?: string; } +/** SendLit Cloud's rule that each ESP needs a healthy feedback connection + * once the organization has sent `requiredAfterSends` emails. */ +export interface FeedbackRequirement { + required: boolean; + requiredAfterSends: number; + sentSoFar: number; +} + +export function getFeedbackRequirement() { + return unwrap(client.feedback.requirement()); +} + export function getEspFeedback(espId: string) { return unwrap(client.feedback.get({ params: { espId } })); } diff --git a/packages/api-contract/src/contract.ts b/packages/api-contract/src/contract.ts index a34dd374..da4988e8 100644 --- a/packages/api-contract/src/contract.ts +++ b/packages/api-contract/src/contract.ts @@ -73,6 +73,7 @@ import { } from "./schemas/events"; import { feedbackConnectionSchema, + feedbackRequirementSchema, testFeedbackConnectionResponseSchema, upsertFeedbackConnectionBodySchema, } from "./schemas/feedback"; @@ -2011,6 +2012,14 @@ const overviewContract = c.router( */ const feedbackContract = c.router( { + requirement: { + method: "GET", + path: "/settings/feedback-requirement", + responses: { 200: feedbackRequirementSchema }, + summary: "Get when ESPs need a delivery-feedback connection", + description: + "On SendLit Cloud, once the organization has sent `requiredAfterSends` emails, SendLit only sends through ESPs with a healthy bounce/complaint feedback connection. `required` is false on self-hosted SendLit.", + }, get: { method: "GET", path: "/settings/esps/:espId/feedback", diff --git a/packages/api-contract/src/mcp-parity.ts b/packages/api-contract/src/mcp-parity.ts index aea138c1..e996a493 100644 --- a/packages/api-contract/src/mcp-parity.ts +++ b/packages/api-contract/src/mcp-parity.ts @@ -308,6 +308,7 @@ export const mcpParityManifest: readonly McpParityManifestEntry[] = [ ), // User ESP delivery feedback + required("feedback.requirement", "get_feedback_requirement", "read"), required("feedback.get", "get_esp_feedback_connection", "read"), required("feedback.upsert", "upsert_esp_feedback_connection", "write"), restOnly( diff --git a/packages/api-contract/src/schemas/feedback.ts b/packages/api-contract/src/schemas/feedback.ts index 2c0c9c83..1dd5c26b 100644 --- a/packages/api-contract/src/schemas/feedback.ts +++ b/packages/api-contract/src/schemas/feedback.ts @@ -64,3 +64,14 @@ export const testFeedbackConnectionResponseSchema = z.object({ success: z.boolean(), error: z.string().optional(), }); + +/** SendLit Cloud's rule that, once an organization has sent its first + * emails, each ESP it sends through needs a healthy feedback connection. */ +export const feedbackRequirementSchema = z.object({ + /** False on self-hosted SendLit, where the rule doesn't apply. */ + required: z.boolean(), + /** Emails the organization can send before the rule applies. */ + requiredAfterSends: z.number().int(), + /** Emails the organization has sent so far, across all its teams. */ + sentSoFar: z.number().int(), +}); diff --git a/packages/email-blocks/src/types.ts b/packages/email-blocks/src/types.ts index 4bc14ac3..fc842d56 100644 --- a/packages/email-blocks/src/types.ts +++ b/packages/email-blocks/src/types.ts @@ -140,14 +140,16 @@ export interface SequenceReport { failed?: string[]; }; /** Set when SendLit paused a running send because of its bounce or - * complaint rate, failures, an ESP error, or an email it couldn't build. */ + * complaint rate, failures, an ESP error, an email it couldn't build, or an + * organization-wide sending stop the team has to fix. */ autoPause?: { reason: | "bounce_rate" | "complaint_rate" | "consecutive_failures" | "esp_error" - | "render_error"; + | "render_error" + | "sending_blocked"; at: number; bounceRate?: number; complaintRate?: number; From abae67618e9b86cfd684e1881e28580a76668264 Mon Sep 17 00:00:00 2001 From: Rajat Date: Wed, 11 Nov 2026 00:22:10 +0530 Subject: [PATCH 2/3] fix(sequences): address review on feedback warnings - Release a 50/90/100% milestone claim when emailing the owners fails, so the next accepted send tries that warning again. - Record the configured test volume on a sending_blocked pause and use it in the owner email and dashboard banner instead of a hard-coded 100. Co-Authored-By: Claude Opus 5.5 --- .../process-ongoing-sequence.test.ts | 10 ++++++++++ .../automation/process-ongoing-sequence.ts | 2 ++ apps/api/src/automation/sending-health.ts | 19 +++++++++++++++---- .../feedback-requirement.test.ts | 19 +++++++++++++++++++ .../delivery-feedback/feedback-requirement.ts | 16 +++++++++++++++- .../dashboard/auto-pause-banner.tsx | 11 +++++++---- packages/email-blocks/src/types.ts | 3 +++ 7 files changed, 71 insertions(+), 9 deletions(-) diff --git a/apps/api/src/automation/process-ongoing-sequence.test.ts b/apps/api/src/automation/process-ongoing-sequence.test.ts index bd2e481d..32eb077c 100644 --- a/apps/api/src/automation/process-ongoing-sequence.test.ts +++ b/apps/api/src/automation/process-ongoing-sequence.test.ts @@ -743,12 +743,19 @@ describe("processOngoingSequence", () => { const notify = spyOn(billingNotifications, "notifyOrganizationOwners") .mockClear() .mockResolvedValue(undefined); + const previousThreshold = process.env.BILLING_TEST_VOLUME_THRESHOLD; + process.env.BILLING_TEST_VOLUME_THRESHOLD = "250"; try { await Promise.all(rows.map((row) => processOngoingSequence(row.id))); // Picked up again after the pause: still nothing sent, nothing failed. await processOngoingSequence(rows[0].id); } finally { resolve.mockRestore(); + if (previousThreshold === undefined) { + delete process.env.BILLING_TEST_VOLUME_THRESHOLD; + } else { + process.env.BILLING_TEST_VOLUME_THRESHOLD = previousThreshold; + } } expect(mockedSendMail).not.toHaveBeenCalled(); @@ -763,6 +770,9 @@ describe("processOngoingSequence", () => { }); expect(notify).toHaveBeenCalledTimes(1); expect(notify.mock.calls[0][2]).toContain("bounces and spam complaints"); + // The configured volume, not a hard-coded 100. + expect(notify.mock.calls[0][2]).toContain("first 250 emails"); + expect((seqAfter.report as any).autoPause.threshold).toBe(250); notify.mockRestore(); const remaining = await tdb .select() diff --git a/apps/api/src/automation/process-ongoing-sequence.ts b/apps/api/src/automation/process-ongoing-sequence.ts index fd061ba3..708b4979 100644 --- a/apps/api/src/automation/process-ongoing-sequence.ts +++ b/apps/api/src/automation/process-ongoing-sequence.ts @@ -1,6 +1,7 @@ import { eq, sql } from "drizzle-orm"; import { ParseError, RenderError, TokenizationError } from "liquidjs"; import { recordPacedSend } from "../abuse/telemetry"; +import { testVolumeThreshold } from "../billing/domains"; import { commitSendReservation, releaseSendReservation } from "../billing/entitlements"; import { isPlanGateError } from "../billing/errors"; import { sequenceBounceLimit } from "../config/constants"; @@ -343,6 +344,7 @@ async function attemptMailSending({ if (!stop) throw err; const paused = await autoPauseSequence(sequence, "sending_blocked", { error: stop, + threshold: testVolumeThreshold(), }); if (paused || (await isSequencePaused(sequence.id))) throw new SequencePausedError(); diff --git a/apps/api/src/automation/sending-health.ts b/apps/api/src/automation/sending-health.ts index 2f23d0d9..74812fc7 100644 --- a/apps/api/src/automation/sending-health.ts +++ b/apps/api/src/automation/sending-health.ts @@ -137,11 +137,15 @@ export function teamFixableSendingStop(error: unknown): string | null { /** Why a `sending_blocked` auto-pause happened and what to do, in words for * the team. */ -export function sendingBlockedReason(code: string): { why: string; next: string } { +export function sendingBlockedReason( + code: string, + /** The cloud test volume when the pause happened (`testVolumeThreshold`). */ + threshold?: number, +): { why: string; next: string } { switch (code) { case "feedback_connection_required": return { - why: "its email provider isn't reporting bounces and spam complaints back to SendLit, which is required after your first 100 emails", + why: `its email provider isn't reporting bounces and spam complaints back to SendLit, which is required after your organization's first ${threshold ?? "few"} emails`, next: "Connect bounce and complaint feedback for the provider in Settings → Email service providers (ESP)", }; case "verified_owner_email_required": @@ -157,6 +161,10 @@ export function sendingBlockedReason(code: string): { why: string; next: string } } +function blockedThreshold(details: Record): number | undefined { + return typeof details.threshold === "number" ? details.threshold : undefined; +} + export async function autoPauseSequence( sequence: Pick< typeof sequences.$inferSelect, @@ -204,13 +212,16 @@ export async function autoPauseSequence( : reason === "render_error" ? renderErrorReason(String(details.error ?? "")) : reason === "sending_blocked" - ? sendingBlockedReason(String(details.error ?? "")).why + ? sendingBlockedReason( + String(details.error ?? ""), + blockedThreshold(details), + ).why : `your email provider refused to send (${String(details.error ?? "")})`; const next = reason === "render_error" ? "Check your mailing address in Settings and the email's content, then resume it from SendLit" : reason === "sending_blocked" - ? `${sendingBlockedReason(String(details.error ?? "")).next}, then resume it from SendLit` + ? `${sendingBlockedReason(String(details.error ?? ""), blockedThreshold(details)).next}, then resume it from SendLit` : "Sending more would hurt your sender reputation. Check your list and your email provider, then resume it from SendLit"; await notifyOrganizationOwners( team.organizationId, diff --git a/apps/api/src/delivery-feedback/feedback-requirement.test.ts b/apps/api/src/delivery-feedback/feedback-requirement.test.ts index 84a3cb7c..31c44bbe 100644 --- a/apps/api/src/delivery-feedback/feedback-requirement.test.ts +++ b/apps/api/src/delivery-feedback/feedback-requirement.test.ts @@ -111,6 +111,25 @@ describe("feedback requirement", () => { } }); + it("tries a warning again on the next send when emailing the owners fails", async () => { + const { team, esp } = await setup(); + const notify = spyOn(notifications, "notifyOrganizationOwners") + .mockRejectedValueOnce(new Error("smtp down")) + .mockResolvedValue(undefined); + try { + await accept(team.id, esp.id, 5); + await notifyFeedbackMilestones(team.id); + expect(notify).toHaveBeenCalledTimes(1); + + await accept(team.id, esp.id, 1); + await notifyFeedbackMilestones(team.id); + expect(notify).toHaveBeenCalledTimes(2); + expect(notify.mock.calls[1][1]).toContain("6 of 10"); + } finally { + notify.mockRestore(); + } + }); + it("doesn't email when every ESP already has healthy feedback", async () => { const { team, esp } = await setup(); await tdb.insert(schema.espFeedbackConnections).values({ diff --git a/apps/api/src/delivery-feedback/feedback-requirement.ts b/apps/api/src/delivery-feedback/feedback-requirement.ts index 45ee4bc0..8755bede 100644 --- a/apps/api/src/delivery-feedback/feedback-requirement.ts +++ b/apps/api/src/delivery-feedback/feedback-requirement.ts @@ -137,7 +137,21 @@ export async function notifyFeedbackMilestones(teamId: string): Promise { threshold, missing.map((esp) => esp.name), ); - await notifyOrganizationOwners(org.id, subject, text); + try { + await notifyOrganizationOwners(org.id, subject, text); + } catch (error) { + // Release the claim so a later send tries this warning again. + await db + .update(organizations) + .set({ feedbackNoticePercent: org.noticePercent }) + .where( + and( + eq(organizations.id, org.id), + eq(organizations.feedbackNoticePercent, reached), + ), + ); + throw error; + } logger.info( { organization_id: org.id, notice_percent: reached }, "feedback connection reminder sent", diff --git a/apps/web/components/dashboard/auto-pause-banner.tsx b/apps/web/components/dashboard/auto-pause-banner.tsx index c00caf6d..94a0a9c1 100644 --- a/apps/web/components/dashboard/auto-pause-banner.tsx +++ b/apps/web/components/dashboard/auto-pause-banner.tsx @@ -24,11 +24,14 @@ function renderErrorReason(code: string): string { } /** Mirrors `sendingBlockedReason` in the API's automation/sending-health.ts. */ -function sendingBlockedReason(code: string): { why: string; next: string } { +function sendingBlockedReason( + code: string, + threshold?: number, +): { why: string; next: string } { switch (code) { case "feedback_connection_required": return { - why: "Your email provider isn't reporting bounces and spam complaints back to SendLit, which is required after your first 100 emails.", + why: `Your email provider isn't reporting bounces and spam complaints back to SendLit, which is required after your organization's first ${threshold ?? "few"} emails.`, next: "Connect bounce and complaint feedback for the provider in Settings → Email service providers (ESP)", }; case "verified_owner_email_required": @@ -67,13 +70,13 @@ export function AutoPauseBanner({ : pause.reason === "render_error" ? renderErrorReason(pause.error ?? "") : pause.reason === "sending_blocked" - ? sendingBlockedReason(pause.error ?? "").why + ? sendingBlockedReason(pause.error ?? "", pause.threshold).why : `Your email provider refused to send: ${pause.error ?? "unknown error"}.`; const next = pause.reason === "render_error" ? "Check your mailing address in Settings and the email's content" : pause.reason === "sending_blocked" - ? sendingBlockedReason(pause.error ?? "").next + ? sendingBlockedReason(pause.error ?? "", pause.threshold).next : "Clean your list or fix your email provider"; return ( diff --git a/packages/email-blocks/src/types.ts b/packages/email-blocks/src/types.ts index fc842d56..07191301 100644 --- a/packages/email-blocks/src/types.ts +++ b/packages/email-blocks/src/types.ts @@ -154,6 +154,9 @@ export interface SequenceReport { bounceRate?: number; complaintRate?: number; error?: string; + /** For `sending_blocked`: the emails an organization could send before + * its ESPs needed delivery feedback. */ + threshold?: number; }; } From 33b03153ae20d760f372261cd1411567828251d4 Mon Sep 17 00:00:00 2001 From: Rajat Date: Wed, 11 Nov 2026 00:32:31 +0530 Subject: [PATCH 3/3] test(mcp): keep the delivery-feedback tool test off the database get_feedback_requirement imported team/queries and the requirement module, which load the real database client, so the mocked MCP test crashed in CI without DB_CONNECTION_STRING. Look the organization up inside getFeedbackRequirementForTeam, mock that one module in the test, and cover the tool. Co-Authored-By: Claude Opus 5.5 --- .../delivery-feedback/feedback-requirement.ts | 13 +++++++++++++ .../api/src/delivery-feedback/feedback-routes.ts | 9 ++++----- apps/api/src/mcp/tools/delivery-feedback.test.ts | 16 ++++++++++++++++ apps/api/src/mcp/tools/delivery-feedback.ts | 7 ++----- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/apps/api/src/delivery-feedback/feedback-requirement.ts b/apps/api/src/delivery-feedback/feedback-requirement.ts index 8755bede..5874110e 100644 --- a/apps/api/src/delivery-feedback/feedback-requirement.ts +++ b/apps/api/src/delivery-feedback/feedback-requirement.ts @@ -34,6 +34,19 @@ export async function getFeedbackRequirement( }; } +/** The requirement for the organization a team belongs to; null for an + * unknown team. */ +export async function getFeedbackRequirementForTeam( + teamId: string, +): Promise { + const [team] = await db + .select({ organizationId: teams.organizationId }) + .from(teams) + .where(eq(teams.id, teamId)) + .limit(1); + return team ? getFeedbackRequirement(team.organizationId) : null; +} + /** The organization's active team ESPs without a healthy feedback * connection: the ones `assertSendingEligibility` stops past the test * volume. */ diff --git a/apps/api/src/delivery-feedback/feedback-routes.ts b/apps/api/src/delivery-feedback/feedback-routes.ts index 0767eff6..47290b44 100644 --- a/apps/api/src/delivery-feedback/feedback-routes.ts +++ b/apps/api/src/delivery-feedback/feedback-routes.ts @@ -6,7 +6,6 @@ import { requireTeam } from "../auth/require-team"; import { feedbackCapableProviders } from "../config/constants"; import { captureEvent } from "../observability"; import { getEspConfigByEspId } from "../settings/esp/queries"; -import { getTeam } from "../team/queries"; import { disableFeedbackConnection, feedbackConnectionReadinessError, @@ -18,7 +17,7 @@ import { feedbackConnectionInputError, toPublicFeedbackConnection, } from "./feedback-connection-shape"; -import { getFeedbackRequirement } from "./feedback-requirement"; +import { getFeedbackRequirementForTeam } from "./feedback-requirement"; const router = Router(); router.use("/settings/esps/:espId/feedback", requireAuth, requireTeam); @@ -28,9 +27,9 @@ const s = initServer(); const impl = s.router(contract.feedback, { requirement: async ({ req }) => { - const team = await getTeam((req as any).teamId); - if (!team) throw new Error("team_not_found"); - return { status: 200, body: await getFeedbackRequirement(team.organizationId) }; + const requirement = await getFeedbackRequirementForTeam((req as any).teamId); + if (!requirement) throw new Error("team_not_found"); + return { status: 200, body: requirement }; }, get: async ({ params, req }) => { const teamId = (req as any).teamId; diff --git a/apps/api/src/mcp/tools/delivery-feedback.test.ts b/apps/api/src/mcp/tools/delivery-feedback.test.ts index e91ba4c8..ea755ad7 100644 --- a/apps/api/src/mcp/tools/delivery-feedback.test.ts +++ b/apps/api/src/mcp/tools/delivery-feedback.test.ts @@ -17,6 +17,7 @@ const mocks = { countSuppressions: vi.fn(), getSuppressionBySuppressionId: vi.fn(), releaseSuppression: vi.fn(), + getFeedbackRequirementForTeam: vi.fn(), }; mock.module("../../settings/esp/queries", () => ({ @@ -39,6 +40,9 @@ mock.module("../../delivery-feedback/delivery-event-queries", () => ({ mock.module("../../delivery-feedback/outbound-queries", () => ({ getOutboundMessagesByIds: mocks.getOutboundMessagesByIds, })); +mock.module("../../delivery-feedback/feedback-requirement", () => ({ + getFeedbackRequirementForTeam: mocks.getFeedbackRequirementForTeam, +})); mock.module("../../delivery-feedback/suppression-queries", () => ({ addOrStrengthenSuppression: mocks.addOrStrengthenSuppression, listSuppressions: mocks.listSuppressions, @@ -113,6 +117,18 @@ describe("MCP delivery-feedback tools", () => { expect((remove.inputSchema as any).shape.confirm).toBeDefined(); }); + it("reports the team's feedback requirement, or not_found for an unknown team", async () => { + const requirement = { required: true, requiredAfterSends: 100, sentSoFar: 37 }; + mocks.getFeedbackRequirementForTeam.mockResolvedValueOnce(requirement); + await expect(call("get_feedback_requirement", {})).resolves.toEqual(requirement); + expect(mocks.getFeedbackRequirementForTeam).toHaveBeenCalledWith("team-1"); + + mocks.getFeedbackRequirementForTeam.mockResolvedValueOnce(null); + await expect(call("get_feedback_requirement", {})).resolves.toMatchObject({ + error: { code: "not_found" }, + }); + }); + it("needs the suppressions:write scope over OAuth", async () => { await expect( call( diff --git a/apps/api/src/mcp/tools/delivery-feedback.ts b/apps/api/src/mcp/tools/delivery-feedback.ts index 67c4efad..f10caa2f 100644 --- a/apps/api/src/mcp/tools/delivery-feedback.ts +++ b/apps/api/src/mcp/tools/delivery-feedback.ts @@ -18,7 +18,7 @@ import { feedbackConnectionInputError, toPublicFeedbackConnection, } from "../../delivery-feedback/feedback-connection-shape"; -import { getFeedbackRequirement } from "../../delivery-feedback/feedback-requirement"; +import { getFeedbackRequirementForTeam } from "../../delivery-feedback/feedback-requirement"; import { getOutboundMessagesByIds } from "../../delivery-feedback/outbound-queries"; import { addOrStrengthenSuppression, @@ -29,7 +29,6 @@ import { type Suppression, } from "../../delivery-feedback/suppression-queries"; import { getEspConfigByEspId, listEspConfigs } from "../../settings/esp/queries"; -import { getTeam } from "../../team/queries"; import type { SendLitMcpTool } from "../context"; import { dedicatedTool } from "../tool"; import { notFound, toolError } from "./responses"; @@ -114,9 +113,7 @@ export const deliveryFeedbackTools = (): SendLitMcpTool[] => [ outputSchema: feedbackRequirementSchema, annotations: { idempotentHint: true, openWorldHint: false }, async handler({ context }) { - const team = await getTeam(context.teamId); - if (!team) return notFound(); - return getFeedbackRequirement(team.organizationId); + return (await getFeedbackRequirementForTeam(context.teamId)) ?? notFound(); }, }),