diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7102cd7..c382bc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -76,6 +76,9 @@ This file records what changes **in the product** – process and session state
- AI service logs use the web/worker format: `time` (ISO 8601, `Z`) instead of `ts`, lower-case pino level labels (`warn`, not `WARNING`), `logger` only on library records.
- The request list reads each page from an index in its sort order instead of sorting all of the
company's requests (#61).
+- The Better Auth tables live in the schema `identity` instead of `auth`, which Supabase reserves for
+ its own Auth service (#60): a fresh database creates `identity` directly; migration 0018 renames an
+ existing `auth` schema only when the app owns it.
### Fixed
- AI verifier: a unit quoted together with the neighbouring table cell (e.g. `60 | Stk.`) is now
diff --git a/docs/technical/architecture.md b/docs/technical/architecture.md
index b021e64..bc110fa 100644
--- a/docs/technical/architecture.md
+++ b/docs/technical/architecture.md
@@ -48,7 +48,7 @@ Deliberately accepted risks – without an entry here a deviation counts as a de
| Exception | Why accepted | Owner | Expires |
|---|---|---|---|
-| No RLS on the `auth` and `pgboss` schemas | Not company-owned business data; reachable only by server code (ADR-0001 D7) | Fluory | 2026-12-31 (review at M3) |
+| No RLS on the `identity` (Better Auth) and `pgboss` schemas | Not company-owned business data; reachable only by server code (ADR-0001 D7) | Fluory | 2026-12-31 (review at M3) |
| Showcase without unattended retries (Vercel Hobby cron once/day) | Showcase only; production runs a worker (D2) | Fluory | when a production-like demo is needed |
| Better Auth admin plugin mounted without any holder of its admin role | ADR-0001 D6 names the plugin; decided in #30: kept – its `banned` field implements deactivation (sign-in blocked by the plugin). Nobody holds `platform-admin`, so `/api/auth/admin/*` rejects every caller (tested); user management runs through `identity` | Fluory | 2026-12-31 (review at M3) |
| Upload endpoint without a per-user rate limit | Authenticated staff only; body bounded by `Content-Length` + `UPLOAD_MAX_REQUEST_BYTES` before reading | Fluory | before any public deployment (#19) |
diff --git a/docs/technical/data-model.md b/docs/technical/data-model.md
index 77d0679..d008699 100644
--- a/docs/technical/data-model.md
+++ b/docs/technical/data-model.md
@@ -9,7 +9,7 @@
| Schema | Owner | Runtime access (`app_rw`) | Tenant isolation |
|---|---|---|---|
| `app` | `app_owner` | DML via default privileges, no CREATE | every table: `company_id` + RLS **enabled and forced**, policy `
_tenant_isolation` |
-| `auth` | `app_owner` | DML on all tables, no CREATE | none – Better Auth data, server code only (exceptions register) |
+| `identity` (until #60: `auth`) | `app_owner` | DML on all tables, no CREATE | none – Better Auth data, server code only (exceptions register) |
| `pgboss` | `app_owner` (deploy step installs schema + queues) | DML only | none – job queue, IDs only (exceptions register) |
| `drizzle` | `app_owner` | none | migration journal |
@@ -24,7 +24,7 @@ query sees zero rows and every write fails.
| Column | Type | Notes | Class |
|---|---|---|---|
| `id` | uuid PK | `gen_random_uuid()` | internal |
-| `company_id` | uuid FK → `auth.organization.id` | tenant key, `ON DELETE RESTRICT` | internal |
+| `company_id` | uuid FK → `identity.organization.id` | tenant key, `ON DELETE RESTRICT` | internal |
| `status` | text | `NEW · PROCESSING · REVIEW · APPROVED · EXPORTED · REJECTED · ERROR` (check constraint) | internal |
| `created_at` | timestamptz | | internal |
| `source` | text | `upload` (mailbox later) | internal |
@@ -60,7 +60,7 @@ All three: `company_id`, forced RLS, composite FKs to the run and request of the
| `id`, `company_id`, `request_id` | uuid | composite FK `(request_id, company_id)` → `requests` | internal |
| `field_key`, `item_index` | text, int | header field key (six, schema v2), or a line-item field key with its position (`item_index`, #25; null for header fields; check `item_index is null or item_index >= 0`, #47) | internal |
| `old_value`, `new_value` | text | value before / after; the newest row is the current value | confidential + personal |
-| `corrected_by`, `created_at` | uuid, timestamptz | who and when; no FK to `auth.user` (like `audit_events.actor_user_id`) – the history must survive a user's removal | personal (staff) |
+| `corrected_by`, `created_at` | uuid, timestamptz | who and when; no FK to `identity.user` (like `audit_events.actor_user_id`) – the history must survive a user's removal | personal (staff) |
Append-only: forced RLS, `app_rw` has INSERT/SELECT only (UPDATE/DELETE/TRUNCATE revoked) – the
history is the correction audit. The page shows a corrected value as `korrigiert`, never as `found`
@@ -103,7 +103,7 @@ The bytes (confidential + personal) live only in object storage; served via `GET
`app_rw` has INSERT and SELECT only (UPDATE/DELETE/TRUNCATE revoked). Written in the same transaction
as the change.
-### `auth.*` – Better Auth 1.7.5 (generated with the Better Auth CLI, timestamps with time zone)
+### `identity.*` – Better Auth 1.7.5 (generated with the Better Auth CLI, timestamps with time zone)
| Table | Content | Class | Purpose |
|---|---|---|---|
@@ -122,9 +122,9 @@ company's first admin; it can never obtain a session.
## Relations
```text
-auth.organization 1─n auth.member n─1 auth.user 1─n auth.session / auth.account
-auth.organization 1─n auth.invitation
-auth.organization 1─n app.requests (company_id)
+identity.organization 1─n identity.member n─1 identity.user 1─n identity.session / identity.account
+identity.organization 1─n identity.invitation
+identity.organization 1─n app.requests (company_id)
app.requests 1─n app.documents (request_id, company_id)
app.requests 0─1 app.requests (duplicate_of_id, company_id)
app.requests 1─n app.extraction_runs (request_id, company_id) 1─n segments / fields
diff --git a/docs/technical/operations.md b/docs/technical/operations.md
index 3e51dee..4f6c69d 100644
--- a/docs/technical/operations.md
+++ b/docs/technical/operations.md
@@ -115,7 +115,7 @@ the secret manager), before the first `setup` run; the first migration refuses t
## Login rate limit and client IP
Better Auth limits `/api/auth/*` per client IP (5 sign-ins/sign-ups per minute, counters in
-`auth.rate_limit`). The IP comes from `AUTH_IP_HEADERS`; that header is only trustworthy when a
+`identity.rate_limit`). The IP comes from `AUTH_IP_HEADERS`; that header is only trustworthy when a
reverse proxy sets it and clients cannot reach the web container directly. Any deployment beyond the
local machine puts a proxy in front and lists it in `AUTH_TRUSTED_PROXIES`. A per-account limit is a
follow-up (not in the pilot).
diff --git a/drizzle.config.ts b/drizzle.config.ts
index cf0bfdf..1b98a89 100644
--- a/drizzle.config.ts
+++ b/drizzle.config.ts
@@ -5,7 +5,7 @@ export default defineConfig({
dialect: "postgresql",
schema: "./src/db/schema/index.ts",
out: "./src/db/migrations",
- schemaFilter: ["app", "auth"],
+ schemaFilter: ["app", "identity"],
migrations: { schema: "drizzle" },
dbCredentials: { url: process.env.MIGRATION_DATABASE_URL ?? "" },
});
diff --git a/src/db/migrations/0001_identity_tenancy.sql b/src/db/migrations/0001_identity_tenancy.sql
index e87aff5..969d98e 100644
--- a/src/db/migrations/0001_identity_tenancy.sql
+++ b/src/db/migrations/0001_identity_tenancy.sql
@@ -1,7 +1,7 @@
-- Generated by drizzle-kit from src/db/schema/*; edited: schema `app` already exists (0000).
-CREATE SCHEMA "auth";
+CREATE SCHEMA "identity";
--> statement-breakpoint
-CREATE TABLE "auth"."account" (
+CREATE TABLE "identity"."account" (
"id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
@@ -17,7 +17,7 @@ CREATE TABLE "auth"."account" (
"updated_at" timestamp with time zone NOT NULL
);
--> statement-breakpoint
-CREATE TABLE "auth"."invitation" (
+CREATE TABLE "identity"."invitation" (
"id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL,
"organization_id" uuid NOT NULL,
"email" text NOT NULL,
@@ -28,7 +28,7 @@ CREATE TABLE "auth"."invitation" (
"inviter_id" uuid NOT NULL
);
--> statement-breakpoint
-CREATE TABLE "auth"."member" (
+CREATE TABLE "identity"."member" (
"id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL,
"organization_id" uuid NOT NULL,
"user_id" uuid NOT NULL,
@@ -36,7 +36,7 @@ CREATE TABLE "auth"."member" (
"created_at" timestamp with time zone NOT NULL
);
--> statement-breakpoint
-CREATE TABLE "auth"."organization" (
+CREATE TABLE "identity"."organization" (
"id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"slug" text NOT NULL,
@@ -46,7 +46,7 @@ CREATE TABLE "auth"."organization" (
CONSTRAINT "organization_slug_unique" UNIQUE("slug")
);
--> statement-breakpoint
-CREATE TABLE "auth"."rate_limit" (
+CREATE TABLE "identity"."rate_limit" (
"id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL,
"key" text NOT NULL,
"count" integer NOT NULL,
@@ -54,7 +54,7 @@ CREATE TABLE "auth"."rate_limit" (
CONSTRAINT "rate_limit_key_unique" UNIQUE("key")
);
--> statement-breakpoint
-CREATE TABLE "auth"."session" (
+CREATE TABLE "identity"."session" (
"id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"token" text NOT NULL,
@@ -68,7 +68,7 @@ CREATE TABLE "auth"."session" (
CONSTRAINT "session_token_unique" UNIQUE("token")
);
--> statement-breakpoint
-CREATE TABLE "auth"."user" (
+CREATE TABLE "identity"."user" (
"id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
@@ -83,7 +83,7 @@ CREATE TABLE "auth"."user" (
CONSTRAINT "user_email_unique" UNIQUE("email")
);
--> statement-breakpoint
-CREATE TABLE "auth"."verification" (
+CREATE TABLE "identity"."verification" (
"id" uuid PRIMARY KEY DEFAULT pg_catalog.gen_random_uuid() NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
@@ -101,19 +101,19 @@ CREATE TABLE "app"."requests" (
);
--> statement-breakpoint
ALTER TABLE "app"."requests" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
-ALTER TABLE "auth"."account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
-ALTER TABLE "auth"."invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "auth"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
-ALTER TABLE "auth"."invitation" ADD CONSTRAINT "invitation_inviter_id_user_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "auth"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
-ALTER TABLE "auth"."member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "auth"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
-ALTER TABLE "auth"."member" ADD CONSTRAINT "member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
-ALTER TABLE "auth"."session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
-ALTER TABLE "app"."requests" ADD CONSTRAINT "requests_company_id_organization_id_fk" FOREIGN KEY ("company_id") REFERENCES "auth"."organization"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
-CREATE INDEX "account_userId_idx" ON "auth"."account" USING btree ("user_id");--> statement-breakpoint
-CREATE INDEX "invitation_organizationId_idx" ON "auth"."invitation" USING btree ("organization_id");--> statement-breakpoint
-CREATE INDEX "invitation_email_idx" ON "auth"."invitation" USING btree ("email");--> statement-breakpoint
-CREATE INDEX "member_organizationId_idx" ON "auth"."member" USING btree ("organization_id");--> statement-breakpoint
-CREATE INDEX "member_userId_idx" ON "auth"."member" USING btree ("user_id");--> statement-breakpoint
-CREATE INDEX "session_userId_idx" ON "auth"."session" USING btree ("user_id");--> statement-breakpoint
-CREATE INDEX "verification_identifier_idx" ON "auth"."verification" USING btree ("identifier");--> statement-breakpoint
+ALTER TABLE "identity"."account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "identity"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "identity"."invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "identity"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "identity"."invitation" ADD CONSTRAINT "invitation_inviter_id_user_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "identity"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "identity"."member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "identity"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "identity"."member" ADD CONSTRAINT "member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "identity"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "identity"."session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "identity"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "app"."requests" ADD CONSTRAINT "requests_company_id_organization_id_fk" FOREIGN KEY ("company_id") REFERENCES "identity"."organization"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "account_userId_idx" ON "identity"."account" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "invitation_organizationId_idx" ON "identity"."invitation" USING btree ("organization_id");--> statement-breakpoint
+CREATE INDEX "invitation_email_idx" ON "identity"."invitation" USING btree ("email");--> statement-breakpoint
+CREATE INDEX "member_organizationId_idx" ON "identity"."member" USING btree ("organization_id");--> statement-breakpoint
+CREATE INDEX "member_userId_idx" ON "identity"."member" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "session_userId_idx" ON "identity"."session" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "verification_identifier_idx" ON "identity"."verification" USING btree ("identifier");--> statement-breakpoint
CREATE INDEX "requests_company_id_idx" ON "app"."requests" USING btree ("company_id");--> statement-breakpoint
CREATE POLICY "requests_tenant_isolation" ON "app"."requests" AS PERMISSIVE FOR ALL TO public USING (company_id = nullif(current_setting('app.company_id', true), '')::uuid) WITH CHECK (company_id = nullif(current_setting('app.company_id', true), '')::uuid);
\ No newline at end of file
diff --git a/src/db/migrations/0002_auth_grants_force_rls.sql b/src/db/migrations/0002_auth_grants_force_rls.sql
index e7373f5..9d91ce6 100644
--- a/src/db/migrations/0002_auth_grants_force_rls.sql
+++ b/src/db/migrations/0002_auth_grants_force_rls.sql
@@ -1,17 +1,17 @@
-- Hand-written (ADR-0001 D7).
--- 1) The runtime role may use the Better Auth tables (schema `auth`, no RLS – exceptions register),
+-- 1) The runtime role may use the Better Auth tables (schema `identity` – named `auth` until #60 –, no RLS – exceptions register),
-- but never create objects there.
-GRANT USAGE ON SCHEMA auth TO app_rw;
+GRANT USAGE ON SCHEMA identity TO app_rw;
--> statement-breakpoint
-REVOKE ALL ON SCHEMA auth FROM PUBLIC;
+REVOKE ALL ON SCHEMA identity FROM PUBLIC;
--> statement-breakpoint
-GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA auth TO app_rw;
+GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA identity TO app_rw;
--> statement-breakpoint
-ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA auth GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_rw;
+ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA identity GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_rw;
--> statement-breakpoint
-- 2) Forced RLS: drizzle-kit emits only ENABLE. FORCE makes the policy apply to the table owner too,
-- so no role except a superuser ever reads company data without `app.company_id`.
ALTER TABLE app.requests FORCE ROW LEVEL SECURITY;
--> statement-breakpoint
-- 3) One company per user in the pilot: the session hook and getActor resolve THE membership.
-CREATE UNIQUE INDEX member_one_company_per_user ON auth.member (user_id);
+CREATE UNIQUE INDEX member_one_company_per_user ON identity.member (user_id);
diff --git a/src/db/migrations/0003_intake.sql b/src/db/migrations/0003_intake.sql
index 46d180f..5ca9685 100644
--- a/src/db/migrations/0003_intake.sql
+++ b/src/db/migrations/0003_intake.sql
@@ -32,8 +32,8 @@ ALTER TABLE "app"."requests" ADD COLUMN "message_id" text;--> statement-breakpoi
ALTER TABLE "app"."requests" ADD COLUMN "fingerprint" text;--> statement-breakpoint
ALTER TABLE "app"."requests" ADD COLUMN "possible_duplicate" boolean DEFAULT false NOT NULL;--> statement-breakpoint
ALTER TABLE "app"."requests" ADD COLUMN "duplicate_of_id" uuid;--> statement-breakpoint
-ALTER TABLE "app"."audit_events" ADD CONSTRAINT "audit_events_company_id_organization_id_fk" FOREIGN KEY ("company_id") REFERENCES "auth"."organization"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
-ALTER TABLE "app"."documents" ADD CONSTRAINT "documents_company_id_organization_id_fk" FOREIGN KEY ("company_id") REFERENCES "auth"."organization"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "app"."audit_events" ADD CONSTRAINT "audit_events_company_id_organization_id_fk" FOREIGN KEY ("company_id") REFERENCES "identity"."organization"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "app"."documents" ADD CONSTRAINT "documents_company_id_organization_id_fk" FOREIGN KEY ("company_id") REFERENCES "identity"."organization"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "app"."documents" ADD CONSTRAINT "documents_request_id_requests_id_fk" FOREIGN KEY ("request_id") REFERENCES "app"."requests"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "audit_events_entity_idx" ON "app"."audit_events" USING btree ("company_id","entity_type","entity_id");--> statement-breakpoint
CREATE INDEX "documents_company_id_idx" ON "app"."documents" USING btree ("company_id");--> statement-breakpoint
diff --git a/src/db/migrations/0006_processing.sql b/src/db/migrations/0006_processing.sql
index 94e8566..a4def8e 100644
--- a/src/db/migrations/0006_processing.sql
+++ b/src/db/migrations/0006_processing.sql
@@ -51,7 +51,7 @@ ALTER TABLE "app"."requests" ADD COLUMN "error_message" text;--> statement-break
ALTER TABLE "app"."requests" ADD COLUMN "attempts" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE "app"."requests" ADD COLUMN "next_retry_at" timestamp with time zone;--> statement-breakpoint
ALTER TABLE "app"."extracted_fields" ADD CONSTRAINT "extracted_fields_run_same_company_fk" FOREIGN KEY ("run_id","company_id") REFERENCES "app"."extraction_runs"("id","company_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
-ALTER TABLE "app"."extraction_runs" ADD CONSTRAINT "extraction_runs_company_id_organization_id_fk" FOREIGN KEY ("company_id") REFERENCES "auth"."organization"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "app"."extraction_runs" ADD CONSTRAINT "extraction_runs_company_id_organization_id_fk" FOREIGN KEY ("company_id") REFERENCES "identity"."organization"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "app"."extraction_runs" ADD CONSTRAINT "extraction_runs_request_same_company_fk" FOREIGN KEY ("request_id","company_id") REFERENCES "app"."requests"("id","company_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "app"."extraction_segments" ADD CONSTRAINT "extraction_segments_run_same_company_fk" FOREIGN KEY ("run_id","company_id") REFERENCES "app"."extraction_runs"("id","company_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "extracted_fields_request_idx" ON "app"."extracted_fields" USING btree ("company_id","request_id");--> statement-breakpoint
diff --git a/src/db/migrations/0011_export.sql b/src/db/migrations/0011_export.sql
index 8a18fbd..ed7cc0b 100644
--- a/src/db/migrations/0011_export.sql
+++ b/src/db/migrations/0011_export.sql
@@ -15,6 +15,6 @@ CREATE TABLE "app"."request_exports" (
);
--> statement-breakpoint
ALTER TABLE "app"."request_exports" ENABLE ROW LEVEL SECURITY;--> statement-breakpoint
-ALTER TABLE "app"."request_exports" ADD CONSTRAINT "request_exports_company_id_organization_id_fk" FOREIGN KEY ("company_id") REFERENCES "auth"."organization"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "app"."request_exports" ADD CONSTRAINT "request_exports_company_id_organization_id_fk" FOREIGN KEY ("company_id") REFERENCES "identity"."organization"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "app"."request_exports" ADD CONSTRAINT "request_exports_request_same_company_fk" FOREIGN KEY ("request_id","company_id") REFERENCES "app"."requests"("id","company_id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE POLICY "request_exports_tenant_isolation" ON "app"."request_exports" AS PERMISSIVE FOR ALL TO public USING (company_id = nullif(current_setting('app.company_id', true), '')::uuid) WITH CHECK (company_id = nullif(current_setting('app.company_id', true), '')::uuid);
\ No newline at end of file
diff --git a/src/db/migrations/0018_identity_schema.sql b/src/db/migrations/0018_identity_schema.sql
new file mode 100644
index 0000000..635e2b3
--- /dev/null
+++ b/src/db/migrations/0018_identity_schema.sql
@@ -0,0 +1,12 @@
+-- #60: Supabase reserves the schema `auth` for its own Auth service, so the Better Auth tables live in
+-- `identity` (the module's name). Migrations 0001–0017 create `identity` directly on a fresh database,
+-- so they never touch a platform-owned `auth` schema. Databases migrated before #60 still have OUR
+-- schema `auth`: it is renamed here – only when it belongs to the migrating role and `identity` does not
+-- exist yet. Renaming keeps tables, data, foreign keys, indexes, grants and default privileges.
+DO $$
+BEGIN
+ IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth' AND pg_get_userbyid(nspowner) = current_user)
+ AND NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'identity') THEN
+ ALTER SCHEMA "auth" RENAME TO "identity";
+ END IF;
+END $$;
diff --git a/src/db/migrations/meta/0018_snapshot.json b/src/db/migrations/meta/0018_snapshot.json
new file mode 100644
index 0000000..fff1f3c
--- /dev/null
+++ b/src/db/migrations/meta/0018_snapshot.json
@@ -0,0 +1,2006 @@
+{
+ "id": "8c8a14b9-dd3c-4858-bdf7-9127405111b6",
+ "prevId": "a3cdd9c7-27dd-40ea-8548-769ec4bd713f",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "identity.account": {
+ "name": "account",
+ "schema": "identity",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "pg_catalog.gen_random_uuid()"
+ },
+ "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": "uuid",
+ "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,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "account_userId_idx": {
+ "name": "account_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "account_user_id_user_id_fk": {
+ "name": "account_user_id_user_id_fk",
+ "tableFrom": "account",
+ "tableTo": "user",
+ "schemaTo": "identity",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "identity.invitation": {
+ "name": "invitation",
+ "schema": "identity",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "pg_catalog.gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "invitation_organizationId_idx": {
+ "name": "invitation_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "invitation_email_idx": {
+ "name": "invitation_email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "invitation_organization_id_organization_id_fk": {
+ "name": "invitation_organization_id_organization_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "organization",
+ "schemaTo": "identity",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "invitation_inviter_id_user_id_fk": {
+ "name": "invitation_inviter_id_user_id_fk",
+ "tableFrom": "invitation",
+ "tableTo": "user",
+ "schemaTo": "identity",
+ "columnsFrom": [
+ "inviter_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "identity.member": {
+ "name": "member",
+ "schema": "identity",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "pg_catalog.gen_random_uuid()"
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "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
+ }
+ },
+ "indexes": {
+ "member_organizationId_idx": {
+ "name": "member_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "member_userId_idx": {
+ "name": "member_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "member_organization_id_organization_id_fk": {
+ "name": "member_organization_id_organization_id_fk",
+ "tableFrom": "member",
+ "tableTo": "organization",
+ "schemaTo": "identity",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "member_user_id_user_id_fk": {
+ "name": "member_user_id_user_id_fk",
+ "tableFrom": "member",
+ "tableTo": "user",
+ "schemaTo": "identity",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "identity.organization": {
+ "name": "organization",
+ "schema": "identity",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "pg_catalog.gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "logo": {
+ "name": "logo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "organization_slug_unique": {
+ "name": "organization_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "identity.rate_limit": {
+ "name": "rate_limit",
+ "schema": "identity",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "pg_catalog.gen_random_uuid()"
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "count": {
+ "name": "count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_request": {
+ "name": "last_request",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "rate_limit_key_unique": {
+ "name": "rate_limit_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "identity.session": {
+ "name": "session",
+ "schema": "identity",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "pg_catalog.gen_random_uuid()"
+ },
+ "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,
+ "default": "now()"
+ },
+ "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": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active_organization_id": {
+ "name": "active_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "impersonated_by": {
+ "name": "impersonated_by",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "session_userId_idx": {
+ "name": "session_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "session_user_id_user_id_fk": {
+ "name": "session_user_id_user_id_fk",
+ "tableFrom": "session",
+ "tableTo": "user",
+ "schemaTo": "identity",
+ "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
+ },
+ "identity.user": {
+ "name": "user",
+ "schema": "identity",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "pg_catalog.gen_random_uuid()"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp 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()"
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "banned": {
+ "name": "banned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "ban_reason": {
+ "name": "ban_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ban_expires": {
+ "name": "ban_expires",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "identity.verification": {
+ "name": "verification",
+ "schema": "identity",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "pg_catalog.gen_random_uuid()"
+ },
+ "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,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "verification_identifier_idx": {
+ "name": "verification_identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "app.audit_events": {
+ "name": "audit_events",
+ "schema": "app",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_user_id": {
+ "name": "actor_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id": {
+ "name": "entity_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "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": {
+ "audit_events_entity_idx": {
+ "name": "audit_events_entity_idx",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_events_company_id_organization_id_fk": {
+ "name": "audit_events_company_id_organization_id_fk",
+ "tableFrom": "audit_events",
+ "tableTo": "organization",
+ "schemaTo": "identity",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {
+ "audit_events_tenant_isolation": {
+ "name": "audit_events_tenant_isolation",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": [
+ "public"
+ ],
+ "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid",
+ "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "app.documents": {
+ "name": "documents",
+ "schema": "app",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "request_id": {
+ "name": "request_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filename": {
+ "name": "filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size_bytes": {
+ "name": "size_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sha256": {
+ "name": "sha256",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_key": {
+ "name": "storage_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "documents_company_id_idx": {
+ "name": "documents_company_id_idx",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "documents_request_id_idx": {
+ "name": "documents_request_id_idx",
+ "columns": [
+ {
+ "expression": "request_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "documents_company_id_organization_id_fk": {
+ "name": "documents_company_id_organization_id_fk",
+ "tableFrom": "documents",
+ "tableTo": "organization",
+ "schemaTo": "identity",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "documents_request_same_company_fk": {
+ "name": "documents_request_same_company_fk",
+ "tableFrom": "documents",
+ "tableTo": "requests",
+ "schemaTo": "app",
+ "columnsFrom": [
+ "request_id",
+ "company_id"
+ ],
+ "columnsTo": [
+ "id",
+ "company_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {
+ "documents_tenant_isolation": {
+ "name": "documents_tenant_isolation",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": [
+ "public"
+ ],
+ "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid",
+ "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid"
+ }
+ },
+ "checkConstraints": {
+ "documents_kind_check": {
+ "name": "documents_kind_check",
+ "value": "kind in ('eml', 'msg', 'pdf', 'xlsx', 'docx')"
+ }
+ },
+ "isRLSEnabled": true
+ },
+ "app.extracted_fields": {
+ "name": "extracted_fields",
+ "schema": "app",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "request_id": {
+ "name": "request_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "field_key": {
+ "name": "field_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "model_status": {
+ "name": "model_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "segment_id": {
+ "name": "segment_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "quote": {
+ "name": "quote",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "item_index": {
+ "name": "item_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "extracted_fields_request_idx": {
+ "name": "extracted_fields_request_idx",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "request_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "extracted_fields_run_same_company_fk": {
+ "name": "extracted_fields_run_same_company_fk",
+ "tableFrom": "extracted_fields",
+ "tableTo": "extraction_runs",
+ "schemaTo": "app",
+ "columnsFrom": [
+ "run_id",
+ "company_id"
+ ],
+ "columnsTo": [
+ "id",
+ "company_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "extracted_fields_request_same_company_fk": {
+ "name": "extracted_fields_request_same_company_fk",
+ "tableFrom": "extracted_fields",
+ "tableTo": "requests",
+ "schemaTo": "app",
+ "columnsFrom": [
+ "request_id",
+ "company_id"
+ ],
+ "columnsTo": [
+ "id",
+ "company_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "extracted_fields_evidence_segment_fk": {
+ "name": "extracted_fields_evidence_segment_fk",
+ "tableFrom": "extracted_fields",
+ "tableTo": "extraction_segments",
+ "schemaTo": "app",
+ "columnsFrom": [
+ "run_id",
+ "document_id",
+ "segment_id"
+ ],
+ "columnsTo": [
+ "run_id",
+ "document_id",
+ "segment_id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "extracted_fields_run_field_item_unique": {
+ "name": "extracted_fields_run_field_item_unique",
+ "nullsNotDistinct": true,
+ "columns": [
+ "run_id",
+ "field_key",
+ "item_index"
+ ]
+ }
+ },
+ "policies": {
+ "extracted_fields_tenant_isolation": {
+ "name": "extracted_fields_tenant_isolation",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": [
+ "public"
+ ],
+ "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid",
+ "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid"
+ }
+ },
+ "checkConstraints": {
+ "extracted_fields_item_index_check": {
+ "name": "extracted_fields_item_index_check",
+ "value": "item_index is null or item_index >= 0"
+ },
+ "extracted_fields_status_check": {
+ "name": "extracted_fields_status_check",
+ "value": "status in ('found', 'uncertain', 'missing', 'unverified')"
+ },
+ "extracted_fields_found_has_evidence": {
+ "name": "extracted_fields_found_has_evidence",
+ "value": "status <> 'found' or (quote is not null and segment_id is not null)"
+ }
+ },
+ "isRLSEnabled": true
+ },
+ "app.extraction_runs": {
+ "name": "extraction_runs",
+ "schema": "app",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "request_id": {
+ "name": "request_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "job_id": {
+ "name": "job_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "model_id": {
+ "name": "model_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "prompt_version": {
+ "name": "prompt_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "schema_version": {
+ "name": "schema_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_tokens": {
+ "name": "total_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "latency_ms": {
+ "name": "latency_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "documents": {
+ "name": "documents",
+ "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": {
+ "extraction_runs_request_idx": {
+ "name": "extraction_runs_request_idx",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "request_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "extraction_runs_company_id_organization_id_fk": {
+ "name": "extraction_runs_company_id_organization_id_fk",
+ "tableFrom": "extraction_runs",
+ "tableTo": "organization",
+ "schemaTo": "identity",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "extraction_runs_request_same_company_fk": {
+ "name": "extraction_runs_request_same_company_fk",
+ "tableFrom": "extraction_runs",
+ "tableTo": "requests",
+ "schemaTo": "app",
+ "columnsFrom": [
+ "request_id",
+ "company_id"
+ ],
+ "columnsTo": [
+ "id",
+ "company_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "extraction_runs_job_id_unique": {
+ "name": "extraction_runs_job_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "job_id"
+ ]
+ },
+ "extraction_runs_id_company_unique": {
+ "name": "extraction_runs_id_company_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "id",
+ "company_id"
+ ]
+ }
+ },
+ "policies": {
+ "extraction_runs_tenant_isolation": {
+ "name": "extraction_runs_tenant_isolation",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": [
+ "public"
+ ],
+ "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid",
+ "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "app.extraction_segments": {
+ "name": "extraction_segments",
+ "schema": "app",
+ "columns": {
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "document_id": {
+ "name": "document_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "segment_id": {
+ "name": "segment_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "locator": {
+ "name": "locator",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "extraction_segments_run_same_company_fk": {
+ "name": "extraction_segments_run_same_company_fk",
+ "tableFrom": "extraction_segments",
+ "tableTo": "extraction_runs",
+ "schemaTo": "app",
+ "columnsFrom": [
+ "run_id",
+ "company_id"
+ ],
+ "columnsTo": [
+ "id",
+ "company_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "extraction_segments_pk": {
+ "name": "extraction_segments_pk",
+ "columns": [
+ "run_id",
+ "document_id",
+ "segment_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {
+ "extraction_segments_tenant_isolation": {
+ "name": "extraction_segments_tenant_isolation",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": [
+ "public"
+ ],
+ "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid",
+ "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid"
+ }
+ },
+ "checkConstraints": {},
+ "isRLSEnabled": true
+ },
+ "app.field_corrections": {
+ "name": "field_corrections",
+ "schema": "app",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "request_id": {
+ "name": "request_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "field_key": {
+ "name": "field_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_index": {
+ "name": "item_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "old_value": {
+ "name": "old_value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "new_value": {
+ "name": "new_value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "corrected_by": {
+ "name": "corrected_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "field_corrections_request_idx": {
+ "name": "field_corrections_request_idx",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "request_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "field_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "field_corrections_request_same_company_fk": {
+ "name": "field_corrections_request_same_company_fk",
+ "tableFrom": "field_corrections",
+ "tableTo": "requests",
+ "schemaTo": "app",
+ "columnsFrom": [
+ "request_id",
+ "company_id"
+ ],
+ "columnsTo": [
+ "id",
+ "company_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {
+ "field_corrections_tenant_isolation": {
+ "name": "field_corrections_tenant_isolation",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": [
+ "public"
+ ],
+ "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid",
+ "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid"
+ }
+ },
+ "checkConstraints": {
+ "field_corrections_item_index_check": {
+ "name": "field_corrections_item_index_check",
+ "value": "item_index is null or item_index >= 0"
+ }
+ },
+ "isRLSEnabled": true
+ },
+ "app.request_exports": {
+ "name": "request_exports",
+ "schema": "app",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "request_id": {
+ "name": "request_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "idempotency_key": {
+ "name": "idempotency_key",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "erp_reference": {
+ "name": "erp_reference",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "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()"
+ },
+ "exported_at": {
+ "name": "exported_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "request_exports_company_id_organization_id_fk": {
+ "name": "request_exports_company_id_organization_id_fk",
+ "tableFrom": "request_exports",
+ "tableTo": "organization",
+ "schemaTo": "identity",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "request_exports_request_same_company_fk": {
+ "name": "request_exports_request_same_company_fk",
+ "tableFrom": "request_exports",
+ "tableTo": "requests",
+ "schemaTo": "app",
+ "columnsFrom": [
+ "request_id",
+ "company_id"
+ ],
+ "columnsTo": [
+ "id",
+ "company_id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "request_exports_request_id_unique": {
+ "name": "request_exports_request_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "request_id"
+ ]
+ }
+ },
+ "policies": {
+ "request_exports_tenant_isolation": {
+ "name": "request_exports_tenant_isolation",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": [
+ "public"
+ ],
+ "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid",
+ "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid"
+ }
+ },
+ "checkConstraints": {
+ "request_exports_status_check": {
+ "name": "request_exports_status_check",
+ "value": "status in ('pending', 'succeeded')"
+ },
+ "request_exports_succeeded_has_reference": {
+ "name": "request_exports_succeeded_has_reference",
+ "value": "status <> 'succeeded' or (erp_reference is not null and exported_at is not null)"
+ }
+ },
+ "isRLSEnabled": true
+ },
+ "app.requests": {
+ "name": "requests",
+ "schema": "app",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "company_id": {
+ "name": "company_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'NEW'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'upload'"
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "message_id": {
+ "name": "message_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "fingerprint": {
+ "name": "fingerprint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "possible_duplicate": {
+ "name": "possible_duplicate",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "duplicate_of_id": {
+ "name": "duplicate_of_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_stage": {
+ "name": "error_stage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "next_retry_at": {
+ "name": "next_retry_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rejection_reason": {
+ "name": "rejection_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duplicate_decision": {
+ "name": "duplicate_decision",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "requests_company_created_idx": {
+ "name": "requests_company_created_idx",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "first"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "first"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "requests_company_message_id_idx": {
+ "name": "requests_company_message_id_idx",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "message_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "requests_company_fingerprint_idx": {
+ "name": "requests_company_fingerprint_idx",
+ "columns": [
+ {
+ "expression": "company_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "fingerprint",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "requests_company_id_organization_id_fk": {
+ "name": "requests_company_id_organization_id_fk",
+ "tableFrom": "requests",
+ "tableTo": "organization",
+ "schemaTo": "identity",
+ "columnsFrom": [
+ "company_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "requests_duplicate_same_company_fk": {
+ "name": "requests_duplicate_same_company_fk",
+ "tableFrom": "requests",
+ "tableTo": "requests",
+ "schemaTo": "app",
+ "columnsFrom": [
+ "duplicate_of_id",
+ "company_id"
+ ],
+ "columnsTo": [
+ "id",
+ "company_id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "requests_id_company_unique": {
+ "name": "requests_id_company_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "id",
+ "company_id"
+ ]
+ }
+ },
+ "policies": {
+ "requests_tenant_isolation": {
+ "name": "requests_tenant_isolation",
+ "as": "PERMISSIVE",
+ "for": "ALL",
+ "to": [
+ "public"
+ ],
+ "using": "company_id = nullif(current_setting('app.company_id', true), '')::uuid",
+ "withCheck": "company_id = nullif(current_setting('app.company_id', true), '')::uuid"
+ }
+ },
+ "checkConstraints": {
+ "requests_status_check": {
+ "name": "requests_status_check",
+ "value": "status in ('NEW', 'PROCESSING', 'REVIEW', 'APPROVED', 'EXPORTED', 'REJECTED', 'ERROR')"
+ },
+ "requests_duplicate_decision_check": {
+ "name": "requests_duplicate_decision_check",
+ "value": "duplicate_decision is null or duplicate_decision in ('distinct', 'duplicate')"
+ }
+ },
+ "isRLSEnabled": true
+ }
+ },
+ "enums": {},
+ "schemas": {
+ "identity": "identity",
+ "app": "app"
+ },
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json
index dd2d753..853cdfe 100644
--- a/src/db/migrations/meta/_journal.json
+++ b/src/db/migrations/meta/_journal.json
@@ -127,6 +127,13 @@
"when": 1790256184345,
"tag": "0017_request_list_index",
"breakpoints": true
+ },
+ {
+ "idx": 18,
+ "version": "7",
+ "when": 1790256343255,
+ "tag": "0018_identity_schema",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/src/db/schema/auth.ts b/src/db/schema/auth.ts
index 7c972e0..26c94aa 100644
--- a/src/db/schema/auth.ts
+++ b/src/db/schema/auth.ts
@@ -13,7 +13,7 @@ import {
index,
} from "drizzle-orm/pg-core";
-export const authSchema = pgSchema("auth");
+export const authSchema = pgSchema("identity");
export const user = authSchema.table("user", {
id: uuid("id")
diff --git a/src/db/schema/index.ts b/src/db/schema/index.ts
index 378ac7f..aba176b 100644
--- a/src/db/schema/index.ts
+++ b/src/db/schema/index.ts
@@ -1,4 +1,4 @@
-// Drizzle schema: `auth` (Better Auth, no RLS – exceptions register) and `app` (company-owned,
+// Drizzle schema: `identity` (Better Auth, no RLS – exceptions register) and `app` (company-owned,
// forced RLS). Migrations in ../migrations are generated from here plus hand-written SQL.
export * from "./auth";
export * from "./app";
diff --git a/tests/integration/helpers/stack.ts b/tests/integration/helpers/stack.ts
index d9f829e..19a39c7 100644
--- a/tests/integration/helpers/stack.ts
+++ b/tests/integration/helpers/stack.ts
@@ -29,7 +29,7 @@ export const PASSWORD = "synthetic-password-123";
*/
const group = () => Math.floor(Math.random() * 0x10000).toString(16).padStart(4, "0");
export const freshIp = () => `2001:db8:${group()}:${group()}::1`;
-/** How Better Auth keys an IPv6 client in `auth.rate_limit` (expanded /64 prefix). */
+/** How Better Auth keys an IPv6 client in `identity.rate_limit` (expanded /64 prefix). */
export const rateLimitKeyPrefix = (ip: string) => `2001:0db8:${ip.split(":")[2]}:${ip.split(":")[3]}:`;
export async function call(
diff --git a/tests/integration/migrations.test.ts b/tests/integration/migrations.test.ts
index 423547f..14e84fe 100644
--- a/tests/integration/migrations.test.ts
+++ b/tests/integration/migrations.test.ts
@@ -1,4 +1,4 @@
-import { readFileSync } from "node:fs";
+import { readdirSync, readFileSync } from "node:fs";
import pg from "pg";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
@@ -59,4 +59,15 @@ describe("first migration", () => {
await expect(rw.query(guard)).rejects.toThrow(/migrations must run as app_owner, not app_rw/);
});
+
+ it("never creates or uses a schema named auth – Supabase reserves it (#60)", () => {
+ const folder = new URL("../../src/db/migrations/", import.meta.url);
+ const files = readdirSync(folder).filter((name) => name.endsWith(".sql"));
+ const usesAuth = /"auth"\s*\.|\bSCHEMA\s+"?auth"?\b|\bauth\.[a-z_]+/i;
+ const offenders = files.filter((name) => !name.startsWith("0018_") && usesAuth.test(readFileSync(new URL(name, folder), "utf8")));
+ expect(offenders).toEqual([]);
+ // The one rename for databases migrated before #60 touches only a schema the migrating role owns.
+ const rename = readFileSync(new URL(files.find((name) => name.startsWith("0018_"))!, folder), "utf8");
+ expect(rename).toMatch(/pg_get_userbyid\(nspowner\) = current_user/);
+ });
});
diff --git a/tests/integration/rls-guard.test.ts b/tests/integration/rls-guard.test.ts
index a0d4612..d949355 100644
--- a/tests/integration/rls-guard.test.ts
+++ b/tests/integration/rls-guard.test.ts
@@ -42,7 +42,7 @@ describe("tenant isolation guard: every app table", () => {
}
});
- it("keeps data in known schemas only: app (guarded) plus the registered exceptions auth, pgboss, drizzle", async () => {
+ it("keeps data in known schemas only: app (guarded) plus the registered exceptions identity, pgboss, drizzle", async () => {
const { rows } = await owner.query<{ schema: string }>(
`select distinct n.nspname as schema from pg_class c join pg_namespace n on n.oid = c.relnamespace
where c.relkind in ('r', 'p', 'm', 'f', 'v') and n.nspname not in ('pg_catalog', 'information_schema') and n.nspname not like 'pg\\_toast%'
@@ -51,7 +51,7 @@ describe("tenant isolation guard: every app table", () => {
// A new schema (or a table in public) must be added here consciously – with a register entry
// in docs/technical/architecture.md if it holds company data without RLS.
- expect(rows.map((row) => row.schema)).toEqual(["app", "auth", "drizzle", "pgboss"]);
+ expect(rows.map((row) => row.schema)).toEqual(["app", "drizzle", "identity", "pgboss"]);
});
it("fails for a deliberately unprotected table (proof that the guard bites)", async () => {