From 16cd907e0d4ba248646282c51b7de112c5ebdf84 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 12 Aug 2026 12:29:13 -0700 Subject: [PATCH 1/7] feat(mobile,trpc): add in-app account deletion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit App Store guideline 5.1.1(v) requires apps with account creation to offer in-app account deletion. Adds a user.deleteAccount mutation — cancels active Stripe subscriptions on organizations where the user is the sole member, deletes those orgs, then deletes the user (sessions, accounts, and memberships cascade via FK) — and a destructive confirm flow in Settings → Account. better-auth's built-in deleteUser was not used: it requires a <1-day-fresh session, which 30-day OAuth sessions fail. --- .../account/AccountSettingsScreen.tsx | 37 +++++++++++++++- .../settings/hooks/useDeleteAccount/index.ts | 1 + .../useDeleteAccount/useDeleteAccount.ts | 29 +++++++++++++ packages/trpc/src/router/user/user.ts | 43 ++++++++++++++++++- 4 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 apps/mobile/screens/(authenticated)/settings/hooks/useDeleteAccount/index.ts create mode 100644 apps/mobile/screens/(authenticated)/settings/hooks/useDeleteAccount/useDeleteAccount.ts diff --git a/apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx b/apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx index 4dc92ebf549..7a7cfcde46c 100644 --- a/apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx +++ b/apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx @@ -1,15 +1,40 @@ -import { ScrollView, View } from "react-native"; +import Ionicons from "@expo/vector-icons/Ionicons"; +import { Alert, ScrollView, View } from "react-native"; import { Text } from "@/components/ui/text"; import { useTheme } from "@/hooks/useTheme"; import { useSession } from "@/lib/auth/client"; import { ListRow } from "@/screens/(authenticated)/components/ListRow"; import { ListRowValue } from "@/screens/(authenticated)/components/ListRowValue"; import { UserAvatar } from "../components/UserAvatar"; +import { useDeleteAccount } from "../hooks/useDeleteAccount"; export function AccountSettingsScreen() { const theme = useTheme(); const { data: session } = useSession(); const user = session?.user; + const { deleteAccount, isDeleting } = useDeleteAccount(); + + const handleDeleteAccount = () => { + Alert.alert( + "Delete account?", + "This permanently deletes your account, along with any workspaces and data in organizations where you are the only member. This cannot be undone.", + [ + { style: "cancel", text: "Cancel" }, + { + style: "destructive", + text: "Delete account", + onPress: () => { + deleteAccount().catch(() => { + Alert.alert( + "Could not delete account", + "Something went wrong. Try again, or contact support@superset.sh.", + ); + }); + }, + }, + ], + ); + }; return ( } isLast /> + + + } + label="Delete account" + destructive + onPress={isDeleting ? undefined : handleDeleteAccount} + isLast + /> ); } diff --git a/apps/mobile/screens/(authenticated)/settings/hooks/useDeleteAccount/index.ts b/apps/mobile/screens/(authenticated)/settings/hooks/useDeleteAccount/index.ts new file mode 100644 index 00000000000..1a89801254d --- /dev/null +++ b/apps/mobile/screens/(authenticated)/settings/hooks/useDeleteAccount/index.ts @@ -0,0 +1 @@ +export { useDeleteAccount } from "./useDeleteAccount"; diff --git a/apps/mobile/screens/(authenticated)/settings/hooks/useDeleteAccount/useDeleteAccount.ts b/apps/mobile/screens/(authenticated)/settings/hooks/useDeleteAccount/useDeleteAccount.ts new file mode 100644 index 00000000000..9f1b0f44f32 --- /dev/null +++ b/apps/mobile/screens/(authenticated)/settings/hooks/useDeleteAccount/useDeleteAccount.ts @@ -0,0 +1,29 @@ +import { useQueryClient } from "@tanstack/react-query"; +import { useRouter } from "expo-router"; +import { useCallback, useState } from "react"; +import { signOut } from "@/lib/auth/client"; +import { apiClient } from "@/lib/trpc/client"; + +export function useDeleteAccount() { + const router = useRouter(); + const queryClient = useQueryClient(); + const [isDeleting, setIsDeleting] = useState(false); + + const deleteAccount = useCallback(async () => { + setIsDeleting(true); + try { + await apiClient.user.deleteAccount.mutate(); + // sessions are already deleted server-side, so signOut may 401 + await signOut().catch(() => {}); + queryClient.clear(); + router.replace("/(auth)/sign-in"); + } catch (error) { + console.error("[account/delete] Failed to delete account:", error); + throw error; + } finally { + setIsDeleting(false); + } + }, [router, queryClient]); + + return { deleteAccount, isDeleting }; +} diff --git a/packages/trpc/src/router/user/user.ts b/packages/trpc/src/router/user/user.ts index aa42b3dbb49..820e7f3199b 100644 --- a/packages/trpc/src/router/user/user.ts +++ b/packages/trpc/src/router/user/user.ts @@ -1,7 +1,8 @@ +import { stripeClient } from "@superset/auth/stripe"; import { db } from "@superset/db/client"; -import { members, users } from "@superset/db/schema"; +import { members, organizations, users } from "@superset/db/schema"; import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; -import { and, desc, eq } from "drizzle-orm"; +import { and, count, desc, eq } from "drizzle-orm"; import { z } from "zod"; import { generateImagePathname, uploadImage } from "../../lib/upload"; @@ -52,6 +53,44 @@ export const userRouter = { return updatedUser; }), + /** Sole-member orgs are deleted too, cancelling their Stripe subscriptions + * first — raw org deletes bypass beforeDeleteOrganization in the auth config. */ + deleteAccount: protectedProcedure.mutation(async ({ ctx }) => { + const userId = ctx.session.user.id; + + const memberships = await db.query.members.findMany({ + where: eq(members.userId, userId), + }); + + for (const membership of memberships) { + const [memberCount] = await db + .select({ value: count() }) + .from(members) + .where(eq(members.organizationId, membership.organizationId)); + if ((memberCount?.value ?? 0) > 1) continue; + + const organization = await db.query.organizations.findFirst({ + where: eq(organizations.id, membership.organizationId), + columns: { id: true, stripeCustomerId: true }, + }); + if (organization?.stripeCustomerId) { + const activeSubscriptions = await stripeClient.subscriptions.list({ + customer: organization.stripeCustomerId, + status: "active", + }); + for (const subscription of activeSubscriptions.data) { + await stripeClient.subscriptions.cancel(subscription.id); + } + } + await db + .delete(organizations) + .where(eq(organizations.id, membership.organizationId)); + } + + await db.delete(users).where(eq(users.id, userId)); + return { success: true }; + }), + completeOnboarding: protectedProcedure.mutation(async ({ ctx }) => { const [updatedUser] = await db .update(users) From cb948c9bc828ea24c6e5ff76322cbb88ce51439b Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 12 Aug 2026 13:02:35 -0700 Subject: [PATCH 2/7] feat(mobile,trpc,auth,db): grace-period account deletion with reactivation --- apps/mobile/app/account-pending-deletion.tsx | 3 + .../account/AccountSettingsScreen.tsx | 3 +- apps/mobile/screens/RootLayout/RootLayout.tsx | 7 +- .../AccountPendingDeletionScreen.tsx | 88 + .../hooks/useDaysUntilPurge/index.ts | 1 + .../useDaysUntilPurge/useDaysUntilPurge.ts | 9 + .../screens/account-pending-deletion/index.ts | 1 + packages/auth/src/server.ts | 46 +- .../db/drizzle/0066_add_user_deleted_at.sql | 1 + packages/db/drizzle/meta/0066_snapshot.json | 6721 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema/auth.ts | 1 + packages/shared/src/constants.ts | 3 + packages/trpc/src/router/admin/admin.ts | 44 +- packages/trpc/src/router/user/user.ts | 100 +- packages/trpc/src/trpc.ts | 19 + 16 files changed, 7019 insertions(+), 35 deletions(-) create mode 100644 apps/mobile/app/account-pending-deletion.tsx create mode 100644 apps/mobile/screens/account-pending-deletion/AccountPendingDeletionScreen.tsx create mode 100644 apps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/index.ts create mode 100644 apps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/useDaysUntilPurge.ts create mode 100644 apps/mobile/screens/account-pending-deletion/index.ts create mode 100644 packages/db/drizzle/0066_add_user_deleted_at.sql create mode 100644 packages/db/drizzle/meta/0066_snapshot.json diff --git a/apps/mobile/app/account-pending-deletion.tsx b/apps/mobile/app/account-pending-deletion.tsx new file mode 100644 index 00000000000..fad4ff83768 --- /dev/null +++ b/apps/mobile/app/account-pending-deletion.tsx @@ -0,0 +1,3 @@ +import { AccountPendingDeletionScreen } from "@/screens/account-pending-deletion"; + +export default AccountPendingDeletionScreen; diff --git a/apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx b/apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx index 7a7cfcde46c..322cecd892e 100644 --- a/apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx +++ b/apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx @@ -1,4 +1,5 @@ import Ionicons from "@expo/vector-icons/Ionicons"; +import { ACCOUNT_DELETION_GRACE_DAYS } from "@superset/shared/constants"; import { Alert, ScrollView, View } from "react-native"; import { Text } from "@/components/ui/text"; import { useTheme } from "@/hooks/useTheme"; @@ -17,7 +18,7 @@ export function AccountSettingsScreen() { const handleDeleteAccount = () => { Alert.alert( "Delete account?", - "This permanently deletes your account, along with any workspaces and data in organizations where you are the only member. This cannot be undone.", + `Your account will be deactivated today and permanently deleted after ${ACCOUNT_DELETION_GRACE_DAYS} days, including workspaces, chat history, and organizations where you are the only member. Changed your mind? Sign back in within ${ACCOUNT_DELETION_GRACE_DAYS} days to restore everything.`, [ { style: "cancel", text: "Cancel" }, { diff --git a/apps/mobile/screens/RootLayout/RootLayout.tsx b/apps/mobile/screens/RootLayout/RootLayout.tsx index 3d0116ad852..95f85bffea3 100644 --- a/apps/mobile/screens/RootLayout/RootLayout.tsx +++ b/apps/mobile/screens/RootLayout/RootLayout.tsx @@ -19,15 +19,20 @@ export function RootLayout() { if (isPending) return null; + const pendingDeletion = !!session?.user.deletedAt; + return ( - + + + + diff --git a/apps/mobile/screens/account-pending-deletion/AccountPendingDeletionScreen.tsx b/apps/mobile/screens/account-pending-deletion/AccountPendingDeletionScreen.tsx new file mode 100644 index 00000000000..d1a5ac0ef4d --- /dev/null +++ b/apps/mobile/screens/account-pending-deletion/AccountPendingDeletionScreen.tsx @@ -0,0 +1,88 @@ +import Ionicons from "@expo/vector-icons/Ionicons"; +import { COMPANY } from "@superset/shared/constants"; +import { useState } from "react"; +import { Alert, Linking, View } from "react-native"; +import { Button } from "@/components/ui/button"; +import { Text } from "@/components/ui/text"; +import { useSignOut } from "@/hooks/useSignOut"; +import { useTheme } from "@/hooks/useTheme"; +import { useSession } from "@/lib/auth/client"; +import { apiClient } from "@/lib/trpc/client"; +import { useDaysUntilPurge } from "./hooks/useDaysUntilPurge"; + +export function AccountPendingDeletionScreen() { + const theme = useTheme(); + const { data: session, refetch } = useSession(); + const { signOut, isSigningOut } = useSignOut(); + const [isReactivating, setIsReactivating] = useState(false); + + const daysRemaining = useDaysUntilPurge(session?.user.deletedAt ?? null); + + const handleReactivate = async () => { + setIsReactivating(true); + try { + await apiClient.user.reactivateAccount.mutate(); + await refetch(); + } catch (error) { + console.error("[account/reactivate] Failed:", error); + Alert.alert( + "Could not reactivate", + error instanceof Error ? error.message : "Something went wrong.", + ); + } finally { + setIsReactivating(false); + } + }; + + return ( + + + + + + + + Account pending deletion + + + Your account is deactivated and will be permanently deleted + {daysRemaining !== null && daysRemaining > 0 + ? ` in ${daysRemaining} ${daysRemaining === 1 ? "day" : "days"}` + : " soon"} + . Reactivate to restore it exactly as you left it. + + + + + + + + + + ); +} diff --git a/apps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/index.ts b/apps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/index.ts new file mode 100644 index 00000000000..97740e6e2df --- /dev/null +++ b/apps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/index.ts @@ -0,0 +1 @@ +export { useDaysUntilPurge } from "./useDaysUntilPurge"; diff --git a/apps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/useDaysUntilPurge.ts b/apps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/useDaysUntilPurge.ts new file mode 100644 index 00000000000..964ca3d4421 --- /dev/null +++ b/apps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/useDaysUntilPurge.ts @@ -0,0 +1,9 @@ +import { ACCOUNT_DELETION_GRACE_DAYS } from "@superset/shared/constants"; + +const DAY_MS = 24 * 60 * 60 * 1000; + +export function useDaysUntilPurge(deletedAt: Date | null): number | null { + if (!deletedAt) return null; + const purgeAt = deletedAt.getTime() + ACCOUNT_DELETION_GRACE_DAYS * DAY_MS; + return Math.max(0, Math.ceil((purgeAt - Date.now()) / DAY_MS)); +} diff --git a/apps/mobile/screens/account-pending-deletion/index.ts b/apps/mobile/screens/account-pending-deletion/index.ts new file mode 100644 index 00000000000..47e16b2f765 --- /dev/null +++ b/apps/mobile/screens/account-pending-deletion/index.ts @@ -0,0 +1 @@ +export { AccountPendingDeletionScreen } from "./AccountPendingDeletionScreen"; diff --git a/packages/auth/src/server.ts b/packages/auth/src/server.ts index 71ad7ebf881..b1639d61aa9 100644 --- a/packages/auth/src/server.ts +++ b/packages/auth/src/server.ts @@ -21,6 +21,11 @@ import { getTrustedVercelPreviewOrigins } from "@superset/shared/vercel-preview- import { Client } from "@upstash/qstash"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { + APIError, + createAuthMiddleware, + getSessionFromCtx, +} from "better-auth/api"; import { bearer, customSession, organization } from "better-auth/plugins"; import { jwt } from "better-auth/plugins/jwt"; import { and, asc, count, desc, eq, inArray, ne, sql } from "drizzle-orm"; @@ -48,9 +53,25 @@ const userOptions = { input: false, fieldName: "onboarded_at", }, + deletedAt: { + type: "date", + required: false, + input: false, + fieldName: "deleted_at", + }, }, } as const; +/** Better-auth endpoints a pending-deletion user may still reach: signing in + * (recovery IS sign-in), learning their status, and signing out. Everything + * else — org management, billing, api keys, JWT minting — is refused. */ +const PENDING_DELETION_ALLOWED_PATH_PREFIXES = [ + "/sign-in", + "/callback", + "/get-session", + "/sign-out", +]; + const NOTIFY_SLACK_URL = `${env.NEXT_PUBLIC_API_URL}/api/integrations/stripe/jobs/notify-slack`; const desktopDevPort = process.env.DESKTOP_VITE_PORT || "5173"; const desktopDevOrigins = @@ -115,6 +136,23 @@ export const auth = betterAuth({ }, }, user: userOptions, + hooks: { + before: createAuthMiddleware(async (ctx) => { + if ( + PENDING_DELETION_ALLOWED_PATH_PREFIXES.some((prefix) => + ctx.path.startsWith(prefix), + ) + ) { + return; + } + const session = await getSessionFromCtx(ctx); + if ((session?.user as { deletedAt?: Date | null })?.deletedAt) { + throw new APIError("FORBIDDEN", { + message: "Account is pending deletion.", + }); + } + }), + }, advanced: { crossSubDomainCookies: { enabled: true, @@ -850,11 +888,15 @@ export const auth = betterAuth({ // explicitly so the onboarding gate is deterministic. const userRow = await db.query.users.findFirst({ where: eq(authSchema.users.id, user.id), - columns: { onboardedAt: true }, + columns: { onboardedAt: true, deletedAt: true }, }); return { - user: { ...user, onboardedAt: userRow?.onboardedAt ?? null }, + user: { + ...user, + onboardedAt: userRow?.onboardedAt ?? null, + deletedAt: userRow?.deletedAt ?? null, + }, session: { ...session, activeOrganizationId, diff --git a/packages/db/drizzle/0066_add_user_deleted_at.sql b/packages/db/drizzle/0066_add_user_deleted_at.sql new file mode 100644 index 00000000000..8c220541401 --- /dev/null +++ b/packages/db/drizzle/0066_add_user_deleted_at.sql @@ -0,0 +1 @@ +ALTER TABLE "auth"."users" ADD COLUMN "deleted_at" timestamp; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0066_snapshot.json b/packages/db/drizzle/meta/0066_snapshot.json new file mode 100644 index 00000000000..fae9149ab5c --- /dev/null +++ b/packages/db/drizzle/meta/0066_snapshot.json @@ -0,0 +1,6721 @@ +{ + "id": "45460919-07c7-4389-b2dc-7a23788fe5d0", + "prevId": "f3d09110-2b42-454a-a12b-f45c58fbcc31", + "version": "7", + "dialect": "postgresql", + "tables": { + "auth.accounts": { + "name": "accounts", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "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", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "accounts_user_id_idx": { + "name": "accounts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_user_id_users_id_fk": { + "name": "accounts_user_id_users_id_fk", + "tableFrom": "accounts", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.apikeys": { + "name": "apikeys", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 86400000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 10 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "CASE\n\t\t\t\tWHEN metadata IS NULL OR metadata = '' THEN NULL\n\t\t\t\tWHEN NOT (metadata IS JSON OBJECT) THEN NULL\n\t\t\t\tWHEN (metadata::jsonb->>'organizationId') ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'\n\t\t\t\t\tTHEN (metadata::jsonb->>'organizationId')::uuid\n\t\t\t\tELSE NULL\n\t\t\tEND", + "type": "stored" + } + } + }, + "indexes": { + "apikeys_configId_idx": { + "name": "apikeys_configId_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikeys_referenceId_idx": { + "name": "apikeys_referenceId_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikeys_key_idx": { + "name": "apikeys_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikeys_organization_id_idx": { + "name": "apikeys_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikeys_metadata_trgm_idx": { + "name": "apikeys_metadata_trgm_idx", + "columns": [ + { + "expression": "\"metadata\" gin_trgm_ops", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.device_codes": { + "name": "device_codes", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.invitations": { + "name": "invitations", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "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", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "inviter_id": { + "name": "inviter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "invitations_organization_id_idx": { + "name": "invitations_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitations_email_idx": { + "name": "invitations_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitations_organization_id_organizations_id_fk": { + "name": "invitations_organization_id_organizations_id_fk", + "tableFrom": "invitations", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_inviter_id_users_id_fk": { + "name": "invitations_inviter_id_users_id_fk", + "tableFrom": "invitations", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitations_team_id_teams_id_fk": { + "name": "invitations_team_id_teams_id_fk", + "tableFrom": "invitations", + "tableTo": "teams", + "schemaTo": "auth", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.jwkss": { + "name": "jwkss", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "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", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.members": { + "name": "members", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "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", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "members_organization_id_idx": { + "name": "members_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "members_user_id_idx": { + "name": "members_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "members_organization_id_organizations_id_fk": { + "name": "members_organization_id_organizations_id_fk", + "tableFrom": "members", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "members_user_id_users_id_fk": { + "name": "members_user_id_users_id_fk", + "tableFrom": "members", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.oauth_access_tokens": { + "name": "oauth_access_tokens", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_id": { + "name": "refresh_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_access_tokens_client_id_oauth_clients_client_id_fk": { + "name": "oauth_access_tokens_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "oauth_clients", + "schemaTo": "auth", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_tokens_session_id_sessions_id_fk": { + "name": "oauth_access_tokens_session_id_sessions_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "sessions", + "schemaTo": "auth", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_access_tokens_user_id_users_id_fk": { + "name": "oauth_access_tokens_user_id_users_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_access_tokens_refresh_id_oauth_refresh_tokens_id_fk": { + "name": "oauth_access_tokens_refresh_id_oauth_refresh_tokens_id_fk", + "tableFrom": "oauth_access_tokens", + "tableTo": "oauth_refresh_tokens", + "schemaTo": "auth", + "columnsFrom": [ + "refresh_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_access_tokens_token_unique": { + "name": "oauth_access_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.oauth_clients": { + "name": "oauth_clients", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_secret": { + "name": "client_secret", + "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 + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "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 + }, + "token_endpoint_auth_method": { + "name": "token_endpoint_auth_method", + "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 + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_clients_user_id_users_id_fk": { + "name": "oauth_clients_user_id_users_id_fk", + "tableFrom": "oauth_clients", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "oauth_clients_client_id_unique": { + "name": "oauth_clients_client_id_unique", + "nullsNotDistinct": false, + "columns": [ + "client_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.oauth_consents": { + "name": "oauth_consents", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_consents_client_id_oauth_clients_client_id_fk": { + "name": "oauth_consents_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_consents", + "tableTo": "oauth_clients", + "schemaTo": "auth", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_consents_user_id_users_id_fk": { + "name": "oauth_consents_user_id_users_id_fk", + "tableFrom": "oauth_consents", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.oauth_refresh_tokens": { + "name": "oauth_refresh_tokens", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "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": "uuid", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "revoked": { + "name": "revoked", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "auth_time": { + "name": "auth_time", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scopes": { + "name": "scopes", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "oauth_refresh_tokens_client_id_oauth_clients_client_id_fk": { + "name": "oauth_refresh_tokens_client_id_oauth_clients_client_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "oauth_clients", + "schemaTo": "auth", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "client_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "oauth_refresh_tokens_session_id_sessions_id_fk": { + "name": "oauth_refresh_tokens_session_id_sessions_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "sessions", + "schemaTo": "auth", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "oauth_refresh_tokens_user_id_users_id_fk": { + "name": "oauth_refresh_tokens_user_id_users_id_fk", + "tableFrom": "oauth_refresh_tokens", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.organizations": { + "name": "organizations", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "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", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_domains": { + "name": "allowed_domains", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + } + }, + "indexes": { + "organizations_slug_idx": { + "name": "organizations_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "organizations_allowed_domains_idx": { + "name": "organizations_allowed_domains_idx", + "columns": [ + { + "expression": "allowed_domains", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.sessions": { + "name": "sessions", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_team_id": { + "name": "active_team_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sessions_user_id_idx": { + "name": "sessions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sessions_token_unique": { + "name": "sessions_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.team_members": { + "name": "team_members", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "team_id": { + "name": "team_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "team_members_team_id_idx": { + "name": "team_members_team_id_idx", + "columns": [ + { + "expression": "team_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_members_user_id_idx": { + "name": "team_members_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_members_organization_id_idx": { + "name": "team_members_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "team_members_team_user_unique": { + "name": "team_members_team_user_unique", + "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", + "schemaTo": "auth", + "columnsFrom": [ + "team_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_user_id_users_id_fk": { + "name": "team_members_user_id_users_id_fk", + "tableFrom": "team_members", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "team_members_organization_id_organizations_id_fk": { + "name": "team_members_organization_id_organizations_id_fk", + "tableFrom": "team_members", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.teams": { + "name": "teams", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "teams_organization_id_idx": { + "name": "teams_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "teams_org_slug_unique": { + "name": "teams_org_slug_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "teams_organization_id_organizations_id_fk": { + "name": "teams_organization_id_organizations_id_fk", + "tableFrom": "teams", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.users": { + "name": "users", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "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 + }, + "organization_ids": { + "name": "organization_ids", + "type": "uuid[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "onboarded_at": { + "name": "onboarded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users_organization_ids_idx": { + "name": "users_organization_ids_idx", + "columns": [ + { + "expression": "organization_ids", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "auth.verifications": { + "name": "verifications", + "schema": "auth", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "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", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verifications_identifier_idx": { + "name": "verifications_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.github_installations": { + "name": "github_installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "installation_id": { + "name": "installation_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "suspended": { + "name": "suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_installations_installation_id_idx": { + "name": "github_installations_installation_id_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_installations_organization_id_organizations_id_fk": { + "name": "github_installations_organization_id_organizations_id_fk", + "tableFrom": "github_installations", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_installations_connected_by_user_id_users_id_fk": { + "name": "github_installations_connected_by_user_id_users_id_fk", + "tableFrom": "github_installations", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "connected_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_installations_installation_id_unique": { + "name": "github_installations_installation_id_unique", + "nullsNotDistinct": false, + "columns": [ + "installation_id" + ] + }, + "github_installations_org_unique": { + "name": "github_installations_org_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_pull_requests": { + "name": "github_pull_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_branch": { + "name": "head_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_branch": { + "name": "base_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_login": { + "name": "author_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_avatar_url": { + "name": "author_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_draft": { + "name": "is_draft", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "additions": { + "name": "additions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "deletions": { + "name": "deletions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "changed_files": { + "name": "changed_files", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "review_decision": { + "name": "review_decision", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checks_status": { + "name": "checks_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "checks": { + "name": "checks", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "merged_at": { + "name": "merged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_pull_requests_repository_id_idx": { + "name": "github_pull_requests_repository_id_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_requests_state_idx": { + "name": "github_pull_requests_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_requests_head_branch_idx": { + "name": "github_pull_requests_head_branch_idx", + "columns": [ + { + "expression": "head_branch", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_pull_requests_org_id_idx": { + "name": "github_pull_requests_org_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_pull_requests_repository_id_github_repositories_id_fk": { + "name": "github_pull_requests_repository_id_github_repositories_id_fk", + "tableFrom": "github_pull_requests", + "tableTo": "github_repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_pull_requests_organization_id_organizations_id_fk": { + "name": "github_pull_requests_organization_id_organizations_id_fk", + "tableFrom": "github_pull_requests", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_pull_requests_repo_pr_unique": { + "name": "github_pull_requests_repo_pr_unique", + "nullsNotDistinct": false, + "columns": [ + "repository_id", + "pr_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_repositories": { + "name": "github_repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "installation_id": { + "name": "installation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "repo_id": { + "name": "repo_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_repositories_installation_id_idx": { + "name": "github_repositories_installation_id_idx", + "columns": [ + { + "expression": "installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_repositories_full_name_idx": { + "name": "github_repositories_full_name_idx", + "columns": [ + { + "expression": "full_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_repositories_org_id_idx": { + "name": "github_repositories_org_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_repositories_installation_id_github_installations_id_fk": { + "name": "github_repositories_installation_id_github_installations_id_fk", + "tableFrom": "github_repositories", + "tableTo": "github_installations", + "columnsFrom": [ + "installation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "github_repositories_organization_id_organizations_id_fk": { + "name": "github_repositories_organization_id_organizations_id_fk", + "tableFrom": "github_repositories", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_repositories_repo_id_unique": { + "name": "github_repositories_repo_id_unique", + "nullsNotDistinct": false, + "columns": [ + "repo_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "ingest.webhook_events": { + "name": "webhook_events", + "schema": "ingest", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "provider": { + "name": "provider", + "type": "integration_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "retry_count": { + "name": "retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "received_at": { + "name": "received_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "webhook_events_provider_status_idx": { + "name": "webhook_events_provider_status_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_events_provider_event_id_idx": { + "name": "webhook_events_provider_event_id_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_events_received_at_idx": { + "name": "webhook_events_received_at_idx", + "columns": [ + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_commands": { + "name": "agent_commands", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_device_id": { + "name": "target_device_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_device_type": { + "name": "target_device_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tool": { + "name": "tool", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "params": { + "name": "params", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "parent_command_id": { + "name": "parent_command_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "command_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "timeout_at": { + "name": "timeout_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "agent_commands_user_status_idx": { + "name": "agent_commands_user_status_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_commands_target_device_status_idx": { + "name": "agent_commands_target_device_status_idx", + "columns": [ + { + "expression": "target_device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_commands_org_created_idx": { + "name": "agent_commands_org_created_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_commands_user_id_users_id_fk": { + "name": "agent_commands_user_id_users_id_fk", + "tableFrom": "agent_commands", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_commands_organization_id_organizations_id_fk": { + "name": "agent_commands_organization_id_organizations_id_fk", + "tableFrom": "agent_commands", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_prompt_versions": { + "name": "automation_prompt_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "automation_id": { + "name": "automation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "window_bucket": { + "name": "window_bucket", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_hash": { + "name": "content_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "automation_prompt_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "restored_from_version_id": { + "name": "restored_from_version_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_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": { + "automation_prompt_versions_bucket_uniq": { + "name": "automation_prompt_versions_bucket_uniq", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_bucket", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"automation_prompt_versions\".\"source\" <> 'restore'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_prompt_versions_automation_idx": { + "name": "automation_prompt_versions_automation_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_prompt_versions_automation_id_automations_id_fk": { + "name": "automation_prompt_versions_automation_id_automations_id_fk", + "tableFrom": "automation_prompt_versions", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_prompt_versions_author_user_id_users_id_fk": { + "name": "automation_prompt_versions_author_user_id_users_id_fk", + "tableFrom": "automation_prompt_versions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "author_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_prompt_versions_restored_from_version_id_fk": { + "name": "automation_prompt_versions_restored_from_version_id_fk", + "tableFrom": "automation_prompt_versions", + "tableTo": "automation_prompt_versions", + "columnsFrom": [ + "restored_from_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automation_runs": { + "name": "automation_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "automation_id": { + "name": "automation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scheduled_for": { + "name": "scheduled_for", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "v2_workspace_id": { + "name": "v2_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "session_kind": { + "name": "session_kind", + "type": "automation_session_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "chat_session_id": { + "name": "chat_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "terminal_session_id": { + "name": "terminal_session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "automation_run_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dispatched_at": { + "name": "dispatched_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": { + "automation_runs_dedup_idx": { + "name": "automation_runs_dedup_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_runs_history_idx": { + "name": "automation_runs_history_idx", + "columns": [ + { + "expression": "automation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_runs_status_idx": { + "name": "automation_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automation_runs_workspace_idx": { + "name": "automation_runs_workspace_idx", + "columns": [ + { + "expression": "v2_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automation_runs_automation_id_automations_id_fk": { + "name": "automation_runs_automation_id_automations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "automations", + "columnsFrom": [ + "automation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_organization_id_organizations_id_fk": { + "name": "automation_runs_organization_id_organizations_id_fk", + "tableFrom": "automation_runs", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automation_runs_chat_session_id_chat_sessions_id_fk": { + "name": "automation_runs_chat_session_id_chat_sessions_id_fk", + "tableFrom": "automation_runs", + "tableTo": "chat_sessions", + "columnsFrom": [ + "chat_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.automations": { + "name": "automations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "owner_user_id": { + "name": "owner_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent": { + "name": "agent", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_host_id": { + "name": "target_host_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "v2_project_id": { + "name": "v2_project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "v2_workspace_id": { + "name": "v2_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rrule": { + "name": "rrule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dtstart": { + "name": "dtstart", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "mcp_scope": { + "name": "mcp_scope", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "next_run_at": { + "name": "next_run_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": { + "automations_dispatcher_idx": { + "name": "automations_dispatcher_idx", + "columns": [ + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automations_owner_idx": { + "name": "automations_owner_idx", + "columns": [ + { + "expression": "owner_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "automations_organization_idx": { + "name": "automations_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "automations_organization_id_organizations_id_fk": { + "name": "automations_organization_id_organizations_id_fk", + "tableFrom": "automations", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "automations_owner_user_id_users_id_fk": { + "name": "automations_owner_user_id_users_id_fk", + "tableFrom": "automations", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "owner_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_attachments": { + "name": "chat_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chat_session_id": { + "name": "chat_session_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "blob_pathname": { + "name": "blob_pathname", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "media_type": { + "name": "media_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_attachments_session_idx": { + "name": "chat_attachments_session_idx", + "columns": [ + { + "expression": "chat_session_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_attachments_created_by_idx": { + "name": "chat_attachments_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_attachments_chat_session_id_chat_sessions_id_fk": { + "name": "chat_attachments_chat_session_id_chat_sessions_id_fk", + "tableFrom": "chat_attachments", + "tableTo": "chat_sessions", + "columnsFrom": [ + "chat_session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_attachments_created_by_users_id_fk": { + "name": "chat_attachments_created_by_users_id_fk", + "tableFrom": "chat_attachments", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_attachments_organization_id_organizations_id_fk": { + "name": "chat_attachments_organization_id_organizations_id_fk", + "tableFrom": "chat_attachments", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.chat_sessions": { + "name": "chat_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "v2_workspace_id": { + "name": "v2_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "chat_sessions_org_idx": { + "name": "chat_sessions_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_sessions_created_by_idx": { + "name": "chat_sessions_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "chat_sessions_last_active_idx": { + "name": "chat_sessions_last_active_idx", + "columns": [ + { + "expression": "last_active_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_sessions_organization_id_organizations_id_fk": { + "name": "chat_sessions_organization_id_organizations_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_sessions_created_by_users_id_fk": { + "name": "chat_sessions_created_by_users_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_sessions_workspace_id_workspaces_id_fk": { + "name": "chat_sessions_workspace_id_workspaces_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "workspaces", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.desktop_notices": { + "name": "desktop_notices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "severity": { + "name": "severity", + "type": "desktop_notice_severity", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "desktop_notice_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'immediate'" + }, + "min_version": { + "name": "min_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "max_version": { + "name": "max_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "platforms": { + "name": "platforms", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "channels": { + "name": "channels", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "starts_at": { + "name": "starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "ends_at": { + "name": "ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "cta_label": { + "name": "cta_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cta_action": { + "name": "cta_action", + "type": "desktop_notice_cta_action", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "cta_url": { + "name": "cta_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dismissible": { + "name": "dismissible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "desktop_notices_active_idx": { + "name": "desktop_notices_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.device_presence": { + "name": "device_presence", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_name": { + "name": "device_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "device_type": { + "name": "device_type", + "type": "device_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "last_seen_at": { + "name": "last_seen_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "device_presence_user_org_idx": { + "name": "device_presence_user_org_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_presence_user_device_idx": { + "name": "device_presence_user_device_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "device_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "device_presence_last_seen_idx": { + "name": "device_presence_last_seen_idx", + "columns": [ + { + "expression": "last_seen_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "device_presence_user_id_users_id_fk": { + "name": "device_presence_user_id_users_id_fk", + "tableFrom": "device_presence", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "device_presence_organization_id_organizations_id_fk": { + "name": "device_presence_organization_id_organizations_id_fk", + "tableFrom": "device_presence", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integration_connections": { + "name": "integration_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connected_by_user_id": { + "name": "connected_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "integration_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_expires_at": { + "name": "token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "disconnect_reason": { + "name": "disconnect_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_org_id": { + "name": "external_org_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_org_name": { + "name": "external_org_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "integration_connections_slack_external_org_active_unique": { + "name": "integration_connections_slack_external_org_active_unique", + "columns": [ + { + "expression": "external_org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"integration_connections\".\"provider\" = 'slack' AND \"integration_connections\".\"disconnected_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "integration_connections_org_idx": { + "name": "integration_connections_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "integration_connections_organization_id_organizations_id_fk": { + "name": "integration_connections_organization_id_organizations_id_fk", + "tableFrom": "integration_connections", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "integration_connections_connected_by_user_id_users_id_fk": { + "name": "integration_connections_connected_by_user_id_users_id_fk", + "tableFrom": "integration_connections", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "connected_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "integration_connections_unique": { + "name": "integration_connections_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "provider" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_repository_id": { + "name": "github_repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repo_owner": { + "name": "repo_owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_name": { + "name": "repo_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projects_organization_id_idx": { + "name": "projects_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_organization_id_organizations_id_fk": { + "name": "projects_organization_id_organizations_id_fk", + "tableFrom": "projects", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "projects_github_repository_id_github_repositories_id_fk": { + "name": "projects_github_repository_id_github_repositories_id_fk", + "tableFrom": "projects", + "tableTo": "github_repositories", + "columnsFrom": [ + "github_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "projects_org_slug_unique": { + "name": "projects_org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sandbox_images": { + "name": "sandbox_images", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "setup_commands": { + "name": "setup_commands", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "base_image": { + "name": "base_image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "system_packages": { + "name": "system_packages", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "sandbox_images_organization_id_idx": { + "name": "sandbox_images_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sandbox_images_organization_id_organizations_id_fk": { + "name": "sandbox_images_organization_id_organizations_id_fk", + "tableFrom": "sandbox_images", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sandbox_images_project_id_projects_id_fk": { + "name": "sandbox_images_project_id_projects_id_fk", + "tableFrom": "sandbox_images", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "sandbox_images_project_unique": { + "name": "sandbox_images_project_unique", + "nullsNotDistinct": false, + "columns": [ + "project_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.secrets": { + "name": "secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_value": { + "name": "encrypted_value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "secrets_project_id_idx": { + "name": "secrets_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "secrets_organization_id_idx": { + "name": "secrets_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "secrets_organization_id_organizations_id_fk": { + "name": "secrets_organization_id_organizations_id_fk", + "tableFrom": "secrets", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "secrets_project_id_projects_id_fk": { + "name": "secrets_project_id_projects_id_fk", + "tableFrom": "secrets", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "secrets_created_by_user_id_users_id_fk": { + "name": "secrets_created_by_user_id_users_id_fk", + "tableFrom": "secrets", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "secrets_project_key_unique": { + "name": "secrets_project_key_unique", + "nullsNotDistinct": false, + "columns": [ + "project_id", + "key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.submitted_prompts": { + "name": "submitted_prompts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "prompt_text": { + "name": "prompt_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "submitter_name": { + "name": "submitter_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "submitted_prompts_user_id_idx": { + "name": "submitted_prompts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "submitted_prompts_organization_id_idx": { + "name": "submitted_prompts_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "submitted_prompts_created_at_idx": { + "name": "submitted_prompts_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "submitted_prompts_user_id_users_id_fk": { + "name": "submitted_prompts_user_id_users_id_fk", + "tableFrom": "submitted_prompts", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "submitted_prompts_organization_id_organizations_id_fk": { + "name": "submitted_prompts_organization_id_organizations_id_fk", + "tableFrom": "submitted_prompts", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscriptions": { + "name": "subscriptions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'incomplete'" + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "cancel_at": { + "name": "cancel_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "canceled_at": { + "name": "canceled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_interval": { + "name": "billing_interval", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_schedule_id": { + "name": "stripe_schedule_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "subscriptions_reference_id_idx": { + "name": "subscriptions_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscriptions_stripe_customer_id_idx": { + "name": "subscriptions_stripe_customer_id_idx", + "columns": [ + { + "expression": "stripe_customer_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "subscriptions_status_idx": { + "name": "subscriptions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "subscriptions_reference_id_organizations_id_fk": { + "name": "subscriptions_reference_id_organizations_id_fk", + "tableFrom": "subscriptions", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "reference_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.task_statuses": { + "name": "task_statuses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position": { + "name": "position", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "progress_percent": { + "name": "progress_percent", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "external_provider": { + "name": "external_provider", + "type": "integration_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "task_statuses_organization_id_idx": { + "name": "task_statuses_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "task_statuses_type_idx": { + "name": "task_statuses_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "task_statuses_organization_id_organizations_id_fk": { + "name": "task_statuses_organization_id_organizations_id_fk", + "tableFrom": "task_statuses", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "task_statuses_org_external_unique": { + "name": "task_statuses_org_external_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "external_provider", + "external_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tasks": { + "name": "tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_id": { + "name": "status_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "task_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "assignee_id": { + "name": "assignee_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "estimate": { + "name": "estimate", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "labels": { + "name": "labels", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_url": { + "name": "pr_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_provider": { + "name": "external_provider", + "type": "integration_provider", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_key": { + "name": "external_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_url": { + "name": "external_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "sync_error": { + "name": "sync_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_project_id": { + "name": "external_project_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_project_name": { + "name": "external_project_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_cycle_id": { + "name": "external_cycle_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_cycle_name": { + "name": "external_cycle_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_external_id": { + "name": "assignee_external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_display_name": { + "name": "assignee_display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_avatar_url": { + "name": "assignee_avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tasks_slug_idx": { + "name": "tasks_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_organization_id_idx": { + "name": "tasks_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_assignee_id_idx": { + "name": "tasks_assignee_id_idx", + "columns": [ + { + "expression": "assignee_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_creator_id_idx": { + "name": "tasks_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_status_id_idx": { + "name": "tasks_status_id_idx", + "columns": [ + { + "expression": "status_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_created_at_idx": { + "name": "tasks_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_external_provider_idx": { + "name": "tasks_external_provider_idx", + "columns": [ + { + "expression": "external_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_external_project_id_idx": { + "name": "tasks_external_project_id_idx", + "columns": [ + { + "expression": "external_project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_external_project_name_idx": { + "name": "tasks_external_project_name_idx", + "columns": [ + { + "expression": "external_project_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_external_cycle_id_idx": { + "name": "tasks_external_cycle_id_idx", + "columns": [ + { + "expression": "external_cycle_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tasks_assignee_external_id_idx": { + "name": "tasks_assignee_external_id_idx", + "columns": [ + { + "expression": "assignee_external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tasks_status_id_task_statuses_id_fk": { + "name": "tasks_status_id_task_statuses_id_fk", + "tableFrom": "tasks", + "tableTo": "task_statuses", + "columnsFrom": [ + "status_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "tasks_organization_id_organizations_id_fk": { + "name": "tasks_organization_id_organizations_id_fk", + "tableFrom": "tasks", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tasks_assignee_id_users_id_fk": { + "name": "tasks_assignee_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "tasks_creator_id_users_id_fk": { + "name": "tasks_creator_id_users_id_fk", + "tableFrom": "tasks", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "tasks_external_unique": { + "name": "tasks_external_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "external_provider", + "external_id" + ] + }, + "tasks_org_slug_unique": { + "name": "tasks_org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users__slack_users": { + "name": "users__slack_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "team_id": { + "name": "team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "model_preference": { + "name": "model_preference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "users__slack_users_user_idx": { + "name": "users__slack_users_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "users__slack_users_org_idx": { + "name": "users__slack_users_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "users__slack_users_user_id_users_id_fk": { + "name": "users__slack_users_user_id_users_id_fk", + "tableFrom": "users__slack_users", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "users__slack_users_organization_id_organizations_id_fk": { + "name": "users__slack_users_organization_id_organizations_id_fk", + "tableFrom": "users__slack_users", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users__slack_users_unique": { + "name": "users__slack_users_unique", + "nullsNotDistinct": false, + "columns": [ + "slack_user_id", + "team_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_clients": { + "name": "v2_clients", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "v2_client_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "v2_clients_organization_id_idx": { + "name": "v2_clients_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_clients_user_id_idx": { + "name": "v2_clients_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_clients_organization_id_organizations_id_fk": { + "name": "v2_clients_organization_id_organizations_id_fk", + "tableFrom": "v2_clients", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_clients_user_id_users_id_fk": { + "name": "v2_clients_user_id_users_id_fk", + "tableFrom": "v2_clients", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "v2_clients_organization_id_user_id_machine_id_pk": { + "name": "v2_clients_organization_id_user_id_machine_id_pk", + "columns": [ + "organization_id", + "user_id", + "machine_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_hosts": { + "name": "v2_hosts", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "machine_id": { + "name": "machine_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_online": { + "name": "is_online", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "wake_command": { + "name": "wake_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_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": { + "v2_hosts_organization_id_idx": { + "name": "v2_hosts_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_hosts_organization_id_organizations_id_fk": { + "name": "v2_hosts_organization_id_organizations_id_fk", + "tableFrom": "v2_hosts", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_hosts_created_by_user_id_users_id_fk": { + "name": "v2_hosts_created_by_user_id_users_id_fk", + "tableFrom": "v2_hosts", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "v2_hosts_organization_id_machine_id_pk": { + "name": "v2_hosts_organization_id_machine_id_pk", + "columns": [ + "organization_id", + "machine_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_projects": { + "name": "v2_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_clone_url": { + "name": "repo_clone_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_repository_id": { + "name": "github_repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "icon_url": { + "name": "icon_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": { + "v2_projects_organization_id_idx": { + "name": "v2_projects_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_projects_organization_id_organizations_id_fk": { + "name": "v2_projects_organization_id_organizations_id_fk", + "tableFrom": "v2_projects", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_projects_github_repository_id_github_repositories_id_fk": { + "name": "v2_projects_github_repository_id_github_repositories_id_fk", + "tableFrom": "v2_projects", + "tableTo": "github_repositories", + "columnsFrom": [ + "github_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "v2_projects_org_slug_unique": { + "name": "v2_projects_org_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "organization_id", + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_users_hosts": { + "name": "v2_users_hosts", + "schema": "", + "columns": { + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "v2_users_host_role", + "typeSchema": "public", + "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": { + "v2_users_hosts_organization_id_idx": { + "name": "v2_users_hosts_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_users_hosts_user_id_idx": { + "name": "v2_users_hosts_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_users_hosts_host_id_idx": { + "name": "v2_users_hosts_host_id_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_users_hosts_organization_id_organizations_id_fk": { + "name": "v2_users_hosts_organization_id_organizations_id_fk", + "tableFrom": "v2_users_hosts", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_users_hosts_user_id_users_id_fk": { + "name": "v2_users_hosts_user_id_users_id_fk", + "tableFrom": "v2_users_hosts", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_users_hosts_host_fk": { + "name": "v2_users_hosts_host_fk", + "tableFrom": "v2_users_hosts", + "tableTo": "v2_hosts", + "columnsFrom": [ + "organization_id", + "host_id" + ], + "columnsTo": [ + "organization_id", + "machine_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "v2_users_hosts_organization_id_user_id_host_id_pk": { + "name": "v2_users_hosts_organization_id_user_id_host_id_pk", + "columns": [ + "organization_id", + "user_id", + "host_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.v2_workspaces": { + "name": "v2_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "host_id": { + "name": "host_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "branch": { + "name": "branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "v2_workspace_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'worktree'" + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "task_id": { + "name": "task_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": { + "v2_workspaces_project_id_idx": { + "name": "v2_workspaces_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_workspaces_organization_id_idx": { + "name": "v2_workspaces_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_workspaces_host_id_idx": { + "name": "v2_workspaces_host_id_idx", + "columns": [ + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_workspaces_task_id_idx": { + "name": "v2_workspaces_task_id_idx", + "columns": [ + { + "expression": "task_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "v2_workspaces_one_main_per_host": { + "name": "v2_workspaces_one_main_per_host", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "host_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"v2_workspaces\".\"type\" = 'main'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "v2_workspaces_organization_id_organizations_id_fk": { + "name": "v2_workspaces_organization_id_organizations_id_fk", + "tableFrom": "v2_workspaces", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_workspaces_project_id_v2_projects_id_fk": { + "name": "v2_workspaces_project_id_v2_projects_id_fk", + "tableFrom": "v2_workspaces", + "tableTo": "v2_projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "v2_workspaces_created_by_user_id_users_id_fk": { + "name": "v2_workspaces_created_by_user_id_users_id_fk", + "tableFrom": "v2_workspaces", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "v2_workspaces_task_id_tasks_id_fk": { + "name": "v2_workspaces_task_id_tasks_id_fk", + "tableFrom": "v2_workspaces", + "tableTo": "tasks", + "columnsFrom": [ + "task_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "v2_workspaces_host_fk": { + "name": "v2_workspaces_host_fk", + "tableFrom": "v2_workspaces", + "tableTo": "v2_hosts", + "columnsFrom": [ + "organization_id", + "host_id" + ], + "columnsTo": [ + "organization_id", + "machine_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspaces": { + "name": "workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "workspace_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspaces_project_id_idx": { + "name": "workspaces_project_id_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspaces_organization_id_idx": { + "name": "workspaces_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspaces_type_idx": { + "name": "workspaces_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspaces_organization_id_organizations_id_fk": { + "name": "workspaces_organization_id_organizations_id_fk", + "tableFrom": "workspaces", + "tableTo": "organizations", + "schemaTo": "auth", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspaces_project_id_projects_id_fk": { + "name": "workspaces_project_id_projects_id_fk", + "tableFrom": "workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspaces_created_by_user_id_users_id_fk": { + "name": "workspaces_created_by_user_id_users_id_fk", + "tableFrom": "workspaces", + "tableTo": "users", + "schemaTo": "auth", + "columnsFrom": [ + "created_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.automation_prompt_source": { + "name": "automation_prompt_source", + "schema": "public", + "values": [ + "human", + "agent", + "restore" + ] + }, + "public.automation_run_status": { + "name": "automation_run_status", + "schema": "public", + "values": [ + "dispatching", + "dispatched", + "skipped_offline", + "dispatch_failed" + ] + }, + "public.automation_session_kind": { + "name": "automation_session_kind", + "schema": "public", + "values": [ + "chat", + "terminal" + ] + }, + "public.command_status": { + "name": "command_status", + "schema": "public", + "values": [ + "pending", + "completed", + "failed", + "timeout" + ] + }, + "public.desktop_notice_cta_action": { + "name": "desktop_notice_cta_action", + "schema": "public", + "values": [ + "install-update", + "open-url" + ] + }, + "public.desktop_notice_severity": { + "name": "desktop_notice_severity", + "schema": "public", + "values": [ + "info", + "warning", + "blocking" + ] + }, + "public.desktop_notice_trigger": { + "name": "desktop_notice_trigger", + "schema": "public", + "values": [ + "immediate", + "pre-update", + "post-update" + ] + }, + "public.device_type": { + "name": "device_type", + "schema": "public", + "values": [ + "desktop", + "mobile", + "web" + ] + }, + "public.integration_provider": { + "name": "integration_provider", + "schema": "public", + "values": [ + "linear", + "github", + "slack" + ] + }, + "public.task_priority": { + "name": "task_priority", + "schema": "public", + "values": [ + "urgent", + "high", + "medium", + "low", + "none" + ] + }, + "public.task_status": { + "name": "task_status", + "schema": "public", + "values": [ + "backlog", + "todo", + "planning", + "working", + "needs-feedback", + "ready-to-merge", + "completed", + "canceled" + ] + }, + "public.v2_client_type": { + "name": "v2_client_type", + "schema": "public", + "values": [ + "desktop", + "mobile", + "web" + ] + }, + "public.v2_users_host_role": { + "name": "v2_users_host_role", + "schema": "public", + "values": [ + "owner", + "member" + ] + }, + "public.v2_workspace_type": { + "name": "v2_workspace_type", + "schema": "public", + "values": [ + "main", + "worktree" + ] + }, + "public.workspace_type": { + "name": "workspace_type", + "schema": "public", + "values": [ + "local", + "cloud" + ] + } + }, + "schemas": { + "auth": "auth", + "ingest": "ingest" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 7f4e31a4e75..f1cc12cbcf2 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -463,6 +463,13 @@ "when": 1786489809031, "tag": "0065_drop_chat_sessions_v2_workspace_fk", "breakpoints": true + }, + { + "idx": 66, + "version": "7", + "when": 1786564882637, + "tag": "0066_add_user_deleted_at", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/auth.ts b/packages/db/src/schema/auth.ts index 4995aefe01f..266aa831f31 100644 --- a/packages/db/src/schema/auth.ts +++ b/packages/db/src/schema/auth.ts @@ -23,6 +23,7 @@ export const users = authSchema.table( image: text("image"), organizationIds: uuid("organization_ids").array().default([]).notNull(), onboardedAt: timestamp("onboarded_at"), + deletedAt: timestamp("deleted_at"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .defaultNow() diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 78ba0a6eb16..68c54a99154 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -72,6 +72,9 @@ export const TOKEN_CONFIG = { // Workspace teardown export const TEARDOWN_TIMEOUT_MS = 60_000; +/** Days a pending-deletion account stays recoverable before it may be purged. */ +export const ACCOUNT_DELETION_GRACE_DAYS = 30; + // PostHog export const POSTHOG_COOKIE_NAME = "superset"; diff --git a/packages/trpc/src/router/admin/admin.ts b/packages/trpc/src/router/admin/admin.ts index 2a234ef3d53..2c2eee172b9 100644 --- a/packages/trpc/src/router/admin/admin.ts +++ b/packages/trpc/src/router/admin/admin.ts @@ -1,8 +1,9 @@ import { auth } from "@superset/auth/server"; +import { stripeClient } from "@superset/auth/stripe"; import { db } from "@superset/db/client"; -import { accounts, users } from "@superset/db/schema"; +import { accounts, members, organizations, users } from "@superset/db/schema"; import type { TRPCRouterRecord } from "@trpc/server"; -import { and, desc, eq } from "drizzle-orm"; +import { and, count, desc, eq, ne } from "drizzle-orm"; import { z } from "zod"; import { adminProcedure } from "../../trpc"; @@ -14,10 +15,47 @@ export const adminRouter = { }); }), + /** Hard purge — the terminal step of account deletion (grace period is + * user-facing, see user.deleteAccount). Sole-member orgs go too, with + * their Stripe subscriptions cancelled first since raw org deletes bypass + * beforeDeleteOrganization in the auth config. Everything else cascades + * from the users row via FK. */ deleteUser: adminProcedure .input(z.object({ userId: z.string() })) .mutation(async ({ input }) => { - // Delete user - Better Auth handles cascading session cleanup + const memberships = await db.query.members.findMany({ + where: eq(members.userId, input.userId), + }); + for (const membership of memberships) { + const [otherMembers] = await db + .select({ value: count() }) + .from(members) + .where( + and( + eq(members.organizationId, membership.organizationId), + ne(members.userId, input.userId), + ), + ); + if ((otherMembers?.value ?? 0) > 0) continue; + + const organization = await db.query.organizations.findFirst({ + where: eq(organizations.id, membership.organizationId), + columns: { id: true, stripeCustomerId: true }, + }); + if (organization?.stripeCustomerId) { + const activeSubscriptions = await stripeClient.subscriptions.list({ + customer: organization.stripeCustomerId, + status: "active", + }); + for (const subscription of activeSubscriptions.data) { + await stripeClient.subscriptions.cancel(subscription.id); + } + } + await db + .delete(organizations) + .where(eq(organizations.id, membership.organizationId)); + } + await db.delete(users).where(eq(users.id, input.userId)); return { success: true }; }), diff --git a/packages/trpc/src/router/user/user.ts b/packages/trpc/src/router/user/user.ts index 820e7f3199b..5d8d6b44835 100644 --- a/packages/trpc/src/router/user/user.ts +++ b/packages/trpc/src/router/user/user.ts @@ -1,8 +1,14 @@ -import { stripeClient } from "@superset/auth/stripe"; import { db } from "@superset/db/client"; -import { members, organizations, users } from "@superset/db/schema"; +import { + members, + oauthAccessTokens, + oauthRefreshTokens, + sessions, + users, +} from "@superset/db/schema"; +import { ACCOUNT_DELETION_GRACE_DAYS } from "@superset/shared/constants"; import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; -import { and, count, desc, eq } from "drizzle-orm"; +import { and, count, desc, eq, ne } from "drizzle-orm"; import { z } from "zod"; import { generateImagePathname, uploadImage } from "../../lib/upload"; @@ -53,41 +59,79 @@ export const userRouter = { return updatedUser; }), - /** Sole-member orgs are deleted too, cancelling their Stripe subscriptions - * first — raw org deletes bypass beforeDeleteOrganization in the auth config. */ + /** Grace-period deletion: marks the account and revokes every live + * credential (sessions + issued OAuth tokens). Orgs and billing are left + * untouched so reactivation within the window restores everything; purge + * happens later via admin.deleteUser. */ deleteAccount: protectedProcedure.mutation(async ({ ctx }) => { const userId = ctx.session.user.id; - const memberships = await db.query.members.findMany({ - where: eq(members.userId, userId), + const ownerships = await db.query.members.findMany({ + where: and(eq(members.userId, userId), eq(members.role, "owner")), }); - - for (const membership of memberships) { - const [memberCount] = await db + for (const ownership of ownerships) { + const [otherMembers] = await db .select({ value: count() }) .from(members) - .where(eq(members.organizationId, membership.organizationId)); - if ((memberCount?.value ?? 0) > 1) continue; - - const organization = await db.query.organizations.findFirst({ - where: eq(organizations.id, membership.organizationId), - columns: { id: true, stripeCustomerId: true }, - }); - if (organization?.stripeCustomerId) { - const activeSubscriptions = await stripeClient.subscriptions.list({ - customer: organization.stripeCustomerId, - status: "active", + .where( + and( + eq(members.organizationId, ownership.organizationId), + ne(members.userId, userId), + ), + ); + if ((otherMembers?.value ?? 0) === 0) continue; + + const [otherOwners] = await db + .select({ value: count() }) + .from(members) + .where( + and( + eq(members.organizationId, ownership.organizationId), + eq(members.role, "owner"), + ne(members.userId, userId), + ), + ); + if ((otherOwners?.value ?? 0) === 0) { + throw new TRPCError({ + code: "PRECONDITION_FAILED", + message: + "You are the only owner of an organization that has other members. Transfer ownership or delete the organization first.", }); - for (const subscription of activeSubscriptions.data) { - await stripeClient.subscriptions.cancel(subscription.id); - } } - await db - .delete(organizations) - .where(eq(organizations.id, membership.organizationId)); } - await db.delete(users).where(eq(users.id, userId)); + await db + .update(users) + .set({ deletedAt: new Date() }) + .where(eq(users.id, userId)); + await db.delete(sessions).where(eq(sessions.userId, userId)); + await db + .delete(oauthAccessTokens) + .where(eq(oauthAccessTokens.userId, userId)); + await db + .delete(oauthRefreshTokens) + .where(eq(oauthRefreshTokens.userId, userId)); + return { success: true }; + }), + + reactivateAccount: protectedProcedure.mutation(async ({ ctx }) => { + const userId = ctx.session.user.id; + + const user = await db.query.users.findFirst({ + where: eq(users.id, userId), + columns: { deletedAt: true }, + }); + if (!user?.deletedAt) return { success: true }; + + const graceMs = ACCOUNT_DELETION_GRACE_DAYS * 24 * 60 * 60 * 1000; + if (Date.now() - user.deletedAt.getTime() > graceMs) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "The recovery period has ended. Contact support@superset.sh.", + }); + } + + await db.update(users).set({ deletedAt: null }).where(eq(users.id, userId)); return { success: true }; }), diff --git a/packages/trpc/src/trpc.ts b/packages/trpc/src/trpc.ts index fc82aa13e7d..bad1518bc31 100644 --- a/packages/trpc/src/trpc.ts +++ b/packages/trpc/src/trpc.ts @@ -91,6 +91,13 @@ const clientTelemetry = t.middleware(async ({ ctx, path, next }) => { export const publicProcedure = t.procedure.use(clientTelemetry); +/** The only procedures a pending-deletion account may call. */ +const PENDING_DELETION_ALLOWED_PROCEDURES = new Set([ + "user.me", + "user.deleteAccount", + "user.reactivateAccount", +]); + export const protectedProcedure = t.procedure .use(clientTelemetry) .use(async ({ ctx, next }) => { @@ -103,6 +110,18 @@ export const protectedProcedure = t.procedure return next({ ctx: { ...ctx, session: ctx.session } }); }) + .use(async ({ ctx, path, next }) => { + if ( + ctx.session.user.deletedAt && + !PENDING_DELETION_ALLOWED_PROCEDURES.has(path) + ) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Account is pending deletion.", + }); + } + return next(); + }) .use(async ({ ctx, next }) => { const sessionOrgId = ctx.session.session.activeOrganizationId ?? null; const headerOrgId = ctx.headers.get(ORGANIZATION_HEADER)?.trim() || null; From 96c2611a7663864dddf65156419d43238b2ca5b8 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 12 Aug 2026 13:58:52 -0700 Subject: [PATCH 3/7] feat(desktop,web,trpc): pending-deletion visibility, clients, and billing-correct purge Deactivated (pending-deletion) users are hidden from member search, assignee pickers/writes, and the Electric users shape; the members settings page shows them greyed with a Deactivated badge so owners can remove them and free the seat. organization.listMembers narrows its user select (it returned the entire users row to every teammate) and filters deactivated members by default behind an includeDeactivated escape hatch. Desktop and web gain pending-deletion screens with reactivation, mirroring mobile. admin.deleteUser now decrements Stripe seat quantity for shared orgs, since the member-row FK cascade never fires afterRemoveMember. --- .../CreateTaskAssigneePicker.tsx | 2 +- .../components/shared/AssigneeMenuItems.tsx | 4 +- .../PendingDeletionScreen.tsx | 81 +++++++++++++++++ .../components/PendingDeletionScreen/index.ts | 1 + .../renderer/routes/_authenticated/layout.tsx | 10 +++ .../MembersSettings/MembersSettings.tsx | 19 +++- .../_authenticated/settings/members/types.ts | 1 + .../OrganizationSettings.tsx | 1 + apps/electric-proxy/src/where.ts | 2 +- .../src/app/account-pending-deletion/page.tsx | 90 +++++++++++++++++++ apps/web/src/proxy.ts | 14 +++ packages/trpc/src/router/admin/admin.ts | 43 ++++++++- .../trpc/src/router/organization/members.ts | 7 +- .../src/router/organization/organization.ts | 44 +++++---- packages/trpc/src/router/task/task.ts | 2 + 15 files changed, 295 insertions(+), 26 deletions(-) create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx create mode 100644 apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/index.ts create mode 100644 apps/web/src/app/account-pending-deletion/page.tsx diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/CreateTaskDialog/components/CreateTaskAssigneePicker/CreateTaskAssigneePicker.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/CreateTaskDialog/components/CreateTaskAssigneePicker/CreateTaskAssigneePicker.tsx index 1855d6b06fa..0116a4d931f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/CreateTaskDialog/components/CreateTaskAssigneePicker/CreateTaskAssigneePicker.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/CreateTaskDialog/components/CreateTaskAssigneePicker/CreateTaskAssigneePicker.tsx @@ -13,7 +13,7 @@ import { useMemo, useState } from "react"; import { HiCheck, HiChevronDown, HiOutlineUserCircle } from "react-icons/hi2"; interface CreateTaskAssigneePickerProps { - users: SelectUser[]; + users: Pick[]; value: string | null; onChange: (value: string | null) => void; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/shared/AssigneeMenuItems.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/shared/AssigneeMenuItems.tsx index e558b807c63..ad3bbf2ed03 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/shared/AssigneeMenuItems.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/shared/AssigneeMenuItems.tsx @@ -9,8 +9,10 @@ interface MenuItemProps { className?: string; } +type AssigneeOption = Pick; + interface AssigneeMenuItemsProps { - users: SelectUser[]; + users: AssigneeOption[]; currentAssigneeId: string | null; hasExternalAssignee?: boolean; onSelect: (userId: string | null) => void; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx new file mode 100644 index 00000000000..d2b441130ef --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx @@ -0,0 +1,81 @@ +import { + ACCOUNT_DELETION_GRACE_DAYS, + COMPANY, +} from "@superset/shared/constants"; +import { Button } from "@superset/ui/button"; +import { useNavigate } from "@tanstack/react-router"; +import { useState } from "react"; +import { HiOutlineExclamationTriangle } from "react-icons/hi2"; +import { useSignOut } from "renderer/hooks/useSignOut"; +import { cloudTrpc } from "renderer/lib/cloud-trpc"; + +const DAY_MS = 24 * 60 * 60 * 1000; + +export function PendingDeletionScreen({ + deletedAt, + onReactivated, +}: { + deletedAt: Date; + onReactivated: () => void; +}) { + const navigate = useNavigate(); + const signOut = useSignOut(); + const [isSigningOut, setIsSigningOut] = useState(false); + + const reactivate = cloudTrpc.user.reactivateAccount.useMutation({ + onSuccess: onReactivated, + }); + + const purgeAt = + new Date(deletedAt).getTime() + ACCOUNT_DELETION_GRACE_DAYS * DAY_MS; + const daysRemaining = Math.max(0, Math.ceil((purgeAt - Date.now()) / DAY_MS)); + + return ( +
+
+ +
+

Account pending deletion

+

+ Your account is deactivated and will be permanently deleted + {daysRemaining > 0 + ? ` in ${daysRemaining} ${daysRemaining === 1 ? "day" : "days"}` + : " soon"} + . Reactivate to restore it exactly as you left it. +

+
+ {reactivate.error && ( +

+ {reactivate.error.message} +

+ )} +
+ + + +
+
+ ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/index.ts new file mode 100644 index 00000000000..8a6cb093385 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/index.ts @@ -0,0 +1 @@ +export { PendingDeletionScreen } from "./PendingDeletionScreen"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx index 01414b14746..b4bd17e612b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx @@ -27,6 +27,7 @@ import { InitGitDialog } from "renderer/react-query/projects/InitGitDialog"; import { DaemonAutoUpdateFailureDialog } from "renderer/routes/_authenticated/components/DaemonAutoUpdateFailureDialog"; import { DashboardNewWorkspaceModal } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal"; import { DiffThemeSync } from "renderer/routes/_authenticated/components/DiffThemeSync"; +import { PendingDeletionScreen } from "renderer/routes/_authenticated/components/PendingDeletionScreen"; import { V1AutoMigration, V1MigrationContinuity, @@ -262,6 +263,15 @@ function AuthenticatedLayout() { return signInRedirect; } + if (session?.user?.deletedAt) { + return ( + void refetch()} + /> + ); + } + if (!activeOrganizationId) { return createOrganizationRedirect; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsx index a9ae2da2af8..2a91c14411c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsx @@ -43,7 +43,7 @@ export function MembersSettings({ visibleItems }: MembersSettingsProps) { ); const { data: membersData, isPending } = - cloudTrpc.organization.listMembers.useQuery(undefined); + cloudTrpc.organization.listMembers.useQuery({ includeDeactivated: true }); const { data: orgData } = cloudTrpc.organization.list.useQuery(undefined); const organization = orgData?.find((org) => org.id === activeOrganizationId); @@ -60,6 +60,7 @@ export function MembersSettings({ visibleItems }: MembersSettingsProps) { name: m.user.name, email: m.user.email, image: m.user.image, + deletedAt: m.user.deletedAt, })) .sort((a, b) => { const priorityDiff = @@ -159,7 +160,13 @@ export function MembersSettings({ visibleItems }: MembersSettingsProps) { image={member.image} />
- + {member.name || "Unknown"} {isCurrentUserRow && ( @@ -170,6 +177,14 @@ export function MembersSettings({ visibleItems }: MembersSettingsProps) { You )} + {member.deletedAt && ( + + Deactivated + + )}
diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/members/types.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/members/types.ts index dcc8cd31f8a..8e985de0fae 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/members/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/members/types.ts @@ -9,4 +9,5 @@ export type TeamMember = { name: string; email: string; image: string | null; + deletedAt: Date | null; }; diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx index ae8607a0956..405dab35f1d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx @@ -138,6 +138,7 @@ export function OrganizationSettings({ name: m.user.name, email: m.user.email, image: m.user.image, + deletedAt: m.user.deletedAt, })) .sort((a, b) => { const priorityDiff = diff --git a/apps/electric-proxy/src/where.ts b/apps/electric-proxy/src/where.ts index 0ae85956a3b..3ff0fe0b78d 100644 --- a/apps/electric-proxy/src/where.ts +++ b/apps/electric-proxy/src/where.ts @@ -101,7 +101,7 @@ export function buildWhereClause( } case "auth.users": { - const fragment = `"organization_ids" @> ARRAY[$1::uuid]`; + const fragment = `"organization_ids" @> ARRAY[$1::uuid] AND "deleted_at" IS NULL`; return { fragment, params: [organizationId] }; } diff --git a/apps/web/src/app/account-pending-deletion/page.tsx b/apps/web/src/app/account-pending-deletion/page.tsx new file mode 100644 index 00000000000..5f02bc81ffc --- /dev/null +++ b/apps/web/src/app/account-pending-deletion/page.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { authClient } from "@superset/auth/client"; +import { + ACCOUNT_DELETION_GRACE_DAYS, + COMPANY, +} from "@superset/shared/constants"; +import { Button } from "@superset/ui/button"; +import { useMutation } from "@tanstack/react-query"; +import { AlertTriangle } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useTRPC } from "@/trpc/react"; + +const DAY_MS = 24 * 60 * 60 * 1000; + +export default function AccountPendingDeletionPage() { + const trpc = useTRPC(); + const router = useRouter(); + const { data: session } = authClient.useSession(); + const [isSigningOut, setIsSigningOut] = useState(false); + + const reactivate = useMutation( + trpc.user.reactivateAccount.mutationOptions({ + onSuccess: () => { + router.replace("/"); + router.refresh(); + }, + }), + ); + + const deletedAt = session?.user.deletedAt; + const daysRemaining = deletedAt + ? Math.max( + 0, + Math.ceil( + (new Date(deletedAt).getTime() + + ACCOUNT_DELETION_GRACE_DAYS * DAY_MS - + Date.now()) / + DAY_MS, + ), + ) + : null; + + return ( +
+ +
+

Account pending deletion

+

+ Your account is deactivated and will be permanently deleted + {daysRemaining !== null && daysRemaining > 0 + ? ` in ${daysRemaining} ${daysRemaining === 1 ? "day" : "days"}` + : " soon"} + . Reactivate to restore it exactly as you left it. +

+
+ {reactivate.error && ( +

{reactivate.error.message}

+ )} +
+ + + +
+
+ ); +} diff --git a/apps/web/src/proxy.ts b/apps/web/src/proxy.ts index 2251abf449d..168e64e34ca 100644 --- a/apps/web/src/proxy.ts +++ b/apps/web/src/proxy.ts @@ -26,6 +26,20 @@ export default async function proxy(req: NextRequest) { return NextResponse.redirect(signInUrl); } + // Pending-deletion accounts only get the recovery page; API routes stay + // reachable so the reactivate mutation itself can go through. + const isApiRoute = + pathname.startsWith("/api") || pathname.startsWith("/trpc"); + if (session?.user.deletedAt && !isApiRoute && !isPublicRoute(pathname)) { + if (pathname !== "/account-pending-deletion") { + return NextResponse.redirect( + new URL("/account-pending-deletion", req.url), + ); + } + } else if (session && pathname === "/account-pending-deletion") { + return NextResponse.redirect(new URL("/", req.url)); + } + if ( session && isInternalRoute(pathname) && diff --git a/packages/trpc/src/router/admin/admin.ts b/packages/trpc/src/router/admin/admin.ts index 2c2eee172b9..1ba37e45531 100644 --- a/packages/trpc/src/router/admin/admin.ts +++ b/packages/trpc/src/router/admin/admin.ts @@ -1,7 +1,13 @@ import { auth } from "@superset/auth/server"; import { stripeClient } from "@superset/auth/stripe"; import { db } from "@superset/db/client"; -import { accounts, members, organizations, users } from "@superset/db/schema"; +import { + accounts, + members, + organizations, + subscriptions, + users, +} from "@superset/db/schema"; import type { TRPCRouterRecord } from "@trpc/server"; import { and, count, desc, eq, ne } from "drizzle-orm"; import { z } from "zod"; @@ -18,8 +24,9 @@ export const adminRouter = { /** Hard purge — the terminal step of account deletion (grace period is * user-facing, see user.deleteAccount). Sole-member orgs go too, with * their Stripe subscriptions cancelled first since raw org deletes bypass - * beforeDeleteOrganization in the auth config. Everything else cascades - * from the users row via FK. */ + * beforeDeleteOrganization. Shared orgs get their seat quantity + * decremented here because the member-row FK cascade never fires + * afterRemoveMember. Deliberately silent — no removal or billing emails. */ deleteUser: adminProcedure .input(z.object({ userId: z.string() })) .mutation(async ({ input }) => { @@ -36,7 +43,35 @@ export const adminRouter = { ne(members.userId, input.userId), ), ); - if ((otherMembers?.value ?? 0) > 0) continue; + const remainingSeats = otherMembers?.value ?? 0; + + if (remainingSeats > 0) { + const subscription = await db.query.subscriptions.findFirst({ + where: and( + eq(subscriptions.referenceId, membership.organizationId), + eq(subscriptions.status, "active"), + ), + }); + if ( + subscription?.stripeSubscriptionId && + subscription.plan !== "enterprise" + ) { + const stripeSub = await stripeClient.subscriptions.retrieve( + subscription.stripeSubscriptionId, + ); + const itemId = stripeSub.items.data[0]?.id; + if (itemId) { + await stripeClient.subscriptions.update( + subscription.stripeSubscriptionId, + { + items: [{ id: itemId, quantity: remainingSeats }], + proration_behavior: "create_prorations", + }, + ); + } + } + continue; + } const organization = await db.query.organizations.findFirst({ where: eq(organizations.id, membership.organizationId), diff --git a/packages/trpc/src/router/organization/members.ts b/packages/trpc/src/router/organization/members.ts index 45185a74d2a..793848ccbfa 100644 --- a/packages/trpc/src/router/organization/members.ts +++ b/packages/trpc/src/router/organization/members.ts @@ -1,7 +1,7 @@ import { db } from "@superset/db/client"; import { members, users } from "@superset/db/schema"; import type { TRPCRouterRecord } from "@trpc/server"; -import { and, eq, ilike, or } from "drizzle-orm"; +import { and, eq, ilike, isNull, or } from "drizzle-orm"; import { z } from "zod"; import { protectedProcedure } from "../../trpc"; import { requireActiveOrgMembership } from "../utils/active-org"; @@ -18,7 +18,10 @@ export const organizationMembersRouter = { ) .query(async ({ ctx, input }) => { const organizationId = await requireActiveOrgMembership(ctx); - const conditions = [eq(members.organizationId, organizationId)]; + const conditions = [ + eq(members.organizationId, organizationId), + isNull(users.deletedAt), + ]; if (input?.search) { const pattern = `%${input.search}%`; const match = or( diff --git a/packages/trpc/src/router/organization/organization.ts b/packages/trpc/src/router/organization/organization.ts index 4b128e4a871..972f8d04402 100644 --- a/packages/trpc/src/router/organization/organization.ts +++ b/packages/trpc/src/router/organization/organization.ts @@ -16,7 +16,7 @@ import { import { findOrgMembership } from "@superset/db/utils"; import { canRemoveMember, type OrganizationRole } from "@superset/shared/auth"; import { TRPCError, type TRPCRouterRecord } from "@trpc/server"; -import { and, desc, eq, ne, sql } from "drizzle-orm"; +import { and, desc, eq, isNull, ne, sql } from "drizzle-orm"; import { z } from "zod"; import { generateImagePathname, uploadImage } from "../../lib/upload"; import { jwtProcedure, protectedProcedure, publicProcedure } from "../../trpc"; @@ -79,20 +79,34 @@ export const organizationRouter = { .orderBy(organizations.name); }), - listMembers: protectedProcedure.query(async ({ ctx }) => { - const organizationId = await requireActiveOrgMembership(ctx); - return db - .select({ - id: members.id, - role: members.role, - createdAt: members.createdAt, - userId: members.userId, - user: users, - }) - .from(members) - .innerJoin(users, eq(members.userId, users.id)) - .where(eq(members.organizationId, organizationId)); - }), + listMembers: protectedProcedure + .input( + z.object({ includeDeactivated: z.boolean().default(false) }).nullish(), + ) + .query(async ({ ctx, input }) => { + const organizationId = await requireActiveOrgMembership(ctx); + const conditions = [eq(members.organizationId, organizationId)]; + if (!input?.includeDeactivated) { + conditions.push(isNull(users.deletedAt)); + } + return db + .select({ + id: members.id, + role: members.role, + createdAt: members.createdAt, + userId: members.userId, + user: { + id: users.id, + name: users.name, + email: users.email, + image: users.image, + deletedAt: users.deletedAt, + }, + }) + .from(members) + .innerJoin(users, eq(members.userId, users.id)) + .where(and(...conditions)); + }), listInvitations: protectedProcedure.query(async ({ ctx }) => { const organizationId = await requireActiveOrgMembership(ctx); diff --git a/packages/trpc/src/router/task/task.ts b/packages/trpc/src/router/task/task.ts index cd399cb97cc..780ae38c289 100644 --- a/packages/trpc/src/router/task/task.ts +++ b/packages/trpc/src/router/task/task.ts @@ -158,10 +158,12 @@ async function getScopedAssigneeId( userId: members.userId, }) .from(members) + .innerJoin(users, eq(members.userId, users.id)) .where( and( eq(members.organizationId, organizationId), eq(members.userId, assigneeId), + isNull(users.deletedAt), ), ) .limit(1); From 434ef87566ad5416aa3fe7d8b78f783d57e43ce5 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 12 Aug 2026 16:14:04 -0700 Subject: [PATCH 4/7] feat(desktop,web): initiate account and org deletion from desktop and web MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete account moves into desktop settings (Account) and a new web /settings/account page, matching mobile; owners also get Delete organization in desktop org settings behind a type-the-name confirm. Deleting the active org follows the leave-organization pattern — setActive the next org, refetch the session, invalidate, and re-enter the root gates — since better-auth nulls the active org server-side and the UI otherwise keeps rendering the deleted one. Confirm-dialog copy trimmed to one sentence everywhere. --- .../AccountSettings/AccountSettings.tsx | 64 +++++++++++ .../OrganizationSettings.tsx | 103 ++++++++++++++++- .../utils/settings-search/settings-search.ts | 33 ++++++ .../account/AccountSettingsScreen.tsx | 2 +- .../components/SidebarNav/SidebarNav.tsx | 1 + .../settings/account/page.tsx | 107 ++++++++++++++++++ 6 files changed, 308 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/app/(dashboard-legacy)/settings/account/page.tsx diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/account/components/AccountSettings/AccountSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/account/components/AccountSettings/AccountSettings.tsx index 5235522021d..610bad92d1f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/account/components/AccountSettings/AccountSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/account/components/AccountSettings/AccountSettings.tsx @@ -1,3 +1,15 @@ +import { ACCOUNT_DELETION_GRACE_DAYS } from "@superset/shared/constants"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@superset/ui/alert-dialog"; import { Avatar } from "@superset/ui/atoms/Avatar"; import { Button } from "@superset/ui/button"; import { Input } from "@superset/ui/input"; @@ -33,6 +45,10 @@ export function AccountSettings({ visibleItems }: AccountSettingsProps) { SETTING_ITEM_ID.ACCOUNT_SIGNOUT, visibleItems, ); + const showDelete = isItemVisible( + SETTING_ITEM_ID.ACCOUNT_DELETE, + visibleItems, + ); const { data: session, @@ -45,9 +61,24 @@ export function AccountSettings({ visibleItems }: AccountSettingsProps) { const [avatarPreview, setAvatarPreview] = useState(null); const signOut = useSignOut(); + const [isDeleting, setIsDeleting] = useState(false); const selectImageMutation = electronTrpc.window.selectImageFile.useMutation(); + async function handleDeleteAccount() { + setIsDeleting(true); + try { + await apiTrpcClient.user.deleteAccount.mutate(); + await signOut(); + } catch (error) { + toast.error( + error instanceof Error ? error.message : "Failed to delete account", + ); + } finally { + setIsDeleting(false); + } + } + useEffect(() => { if (!user) return; setNameValue(user.name ?? ""); @@ -169,6 +200,39 @@ export function AccountSettings({ visibleItems }: AccountSettingsProps) { )} + + {showDelete && ( +
+ + + + + + + + Delete account? + + All of your data will be permanently deleted after{" "} + {ACCOUNT_DELETION_GRACE_DAYS} days — sign back in before + then to restore your account. + + + + Cancel + + Delete account + + + + + +
+ )} ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx index 405dab35f1d..93dcebef184 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx @@ -3,8 +3,20 @@ import { getRoleSortPriority, type OrganizationRole, } from "@superset/shared/auth"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@superset/ui/alert-dialog"; import { Avatar } from "@superset/ui/atoms/Avatar"; import { Badge } from "@superset/ui/badge"; +import { Button } from "@superset/ui/button"; import { Input } from "@superset/ui/input"; import { Label } from "@superset/ui/label"; import { Skeleton } from "@superset/ui/skeleton"; @@ -18,6 +30,7 @@ import { TableRow, } from "@superset/ui/table"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { useNavigate } from "@tanstack/react-router"; import { useEffect, useMemo, useState } from "react"; import { HiOutlineClipboardDocument, @@ -79,14 +92,17 @@ function SettingsRow({ label, hint, htmlFor, children }: SettingsRowProps) { export function OrganizationSettings({ visibleItems, }: OrganizationSettingsProps) { - const { data: session } = authClient.useSession(); + const { data: session, refetch: refetchSession } = authClient.useSession(); const activeOrganizationId = session?.session?.activeOrganizationId; const utils = cloudTrpc.useUtils(); + const navigate = useNavigate(); const searchQuery = useSettingsSearchQuery(); const [isSlugDialogOpen, setIsSlugDialogOpen] = useState(false); const [logoPreview, setLogoPreview] = useState(null); const [nameValue, setNameValue] = useState(""); + const [deleteConfirmValue, setDeleteConfirmValue] = useState(""); + const [isDeletingOrg, setIsDeletingOrg] = useState(false); const { data: organizations, isPending } = cloudTrpc.organization.list.useQuery(undefined); @@ -118,6 +134,10 @@ export function OrganizationSettings({ ); const showId = isItemVisible(SETTING_ITEM_ID.ORGANIZATION_ID, visibleItems); const { copyToClipboard, copied } = useCopyToClipboard(); + const showDelete = isItemVisible( + SETTING_ITEM_ID.ORGANIZATION_DELETE, + visibleItems, + ); const showMembersList = isItemVisible( SETTING_ITEM_ID.ORGANIZATION_MEMBERS_LIST, visibleItems, @@ -193,6 +213,42 @@ export function OrganizationSettings({ } } + async function deleteOrganization(): Promise { + if (!organization) return; + const { error } = await authClient.organization.delete({ + organizationId: organization.id, + }); + if (error) throw new Error(error.message); + + // The server nulls the active org during deletion; explicitly move the + // session to the next org (or none) and re-enter the root gates, same + // as the leave-organization flow. + const remaining = (organizations ?? []).filter( + (o) => o.id !== organization.id, + ); + await authClient.organization.setActive({ + organizationId: remaining[0]?.id ?? null, + }); + await refetchSession(); + await utils.invalidate(); + navigate({ to: "/" }); + } + + function handleDeleteOrganization(): void { + setIsDeletingOrg(true); + toast.promise( + deleteOrganization().finally(() => { + setIsDeletingOrg(false); + setDeleteConfirmValue(""); + }), + { + loading: "Deleting organization...", + success: "Organization deleted", + error: (err) => err.message || "Failed to delete organization", + }, + ); + } + async function handleNameBlur(): Promise { if (!organization || nameValue === organization.name) return; @@ -513,6 +569,51 @@ export function OrganizationSettings({ )} )} + + {showDelete && isOwner && ( +
+ + { + if (!open) setDeleteConfirmValue(""); + }} + > + + + + + + + Delete {organization.name}? + + + All of the organization's data will be permanently + deleted for every member — this cannot be undone. Type + the organization name to confirm. + + + setDeleteConfirmValue(e.target.value)} + placeholder={organization.name} + /> + + Cancel + + Delete organization + + + + + +
+ )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts b/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts index 2644cae749e..afc603fcc73 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/utils/settings-search/settings-search.ts @@ -3,6 +3,7 @@ import type { SettingsSection } from "renderer/stores/settings-state"; export const SETTING_ITEM_ID = { ACCOUNT_PROFILE: "account-profile", ACCOUNT_SIGNOUT: "account-signout", + ACCOUNT_DELETE: "account-delete", ORGANIZATION_LOGO: "organization-logo", ORGANIZATION_NAME: "organization-name", @@ -12,6 +13,7 @@ export const SETTING_ITEM_ID = { ORGANIZATION_MEMBERS_INVITE: "organization-members-invite", ORGANIZATION_MEMBERS_PENDING_INVITATIONS: "organization-members-pending-invitations", + ORGANIZATION_DELETE: "organization-delete", TEAMS_LIST: "teams-list", @@ -119,6 +121,7 @@ export type SettingVariant = "v1" | "v2" | "shared"; export const SETTING_ITEM_VARIANT: Record = { [SETTING_ITEM_ID.ACCOUNT_PROFILE]: "shared", [SETTING_ITEM_ID.ACCOUNT_SIGNOUT]: "shared", + [SETTING_ITEM_ID.ACCOUNT_DELETE]: "shared", [SETTING_ITEM_ID.ORGANIZATION_LOGO]: "shared", [SETTING_ITEM_ID.ORGANIZATION_NAME]: "shared", @@ -127,6 +130,7 @@ export const SETTING_ITEM_VARIANT: Record = { [SETTING_ITEM_ID.ORGANIZATION_MEMBERS_LIST]: "shared", [SETTING_ITEM_ID.ORGANIZATION_MEMBERS_INVITE]: "shared", [SETTING_ITEM_ID.ORGANIZATION_MEMBERS_PENDING_INVITATIONS]: "shared", + [SETTING_ITEM_ID.ORGANIZATION_DELETE]: "shared", [SETTING_ITEM_ID.TEAMS_LIST]: "shared", @@ -251,6 +255,21 @@ export const SETTINGS_ITEMS: SettingsItem[] = [ "leave", ], }, + { + id: SETTING_ITEM_ID.ACCOUNT_DELETE, + section: "account", + title: "Delete Account", + description: "Permanently delete your account", + keywords: [ + "account", + "delete", + "remove", + "close", + "deactivate", + "gdpr", + "erase", + ], + }, { id: SETTING_ITEM_ID.ORGANIZATION_LOGO, section: "organization", @@ -367,6 +386,20 @@ export const SETTINGS_ITEMS: SettingsItem[] = [ "email", ], }, + { + id: SETTING_ITEM_ID.ORGANIZATION_DELETE, + section: "organization", + title: "Delete Organization", + description: "Permanently delete this organization", + keywords: [ + "organization", + "delete", + "remove", + "close", + "disband", + "danger", + ], + }, { id: SETTING_ITEM_ID.TEAMS_LIST, section: "teams", diff --git a/apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx b/apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx index 322cecd892e..40f7176b4b4 100644 --- a/apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx +++ b/apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx @@ -18,7 +18,7 @@ export function AccountSettingsScreen() { const handleDeleteAccount = () => { Alert.alert( "Delete account?", - `Your account will be deactivated today and permanently deleted after ${ACCOUNT_DELETION_GRACE_DAYS} days, including workspaces, chat history, and organizations where you are the only member. Changed your mind? Sign back in within ${ACCOUNT_DELETION_GRACE_DAYS} days to restore everything.`, + `All of your data will be permanently deleted after ${ACCOUNT_DELETION_GRACE_DAYS} days — sign back in before then to restore your account.`, [ { style: "cancel", text: "Cancel" }, { diff --git a/apps/web/src/app/(dashboard-legacy)/components/SidebarNav/SidebarNav.tsx b/apps/web/src/app/(dashboard-legacy)/components/SidebarNav/SidebarNav.tsx index bd7041e82c6..e58a0941625 100644 --- a/apps/web/src/app/(dashboard-legacy)/components/SidebarNav/SidebarNav.tsx +++ b/apps/web/src/app/(dashboard-legacy)/components/SidebarNav/SidebarNav.tsx @@ -7,6 +7,7 @@ import { usePathname } from "next/navigation"; const navItems = [ { href: "/", label: "Home" }, { href: "/integrations", label: "Integrations" }, + { href: "/settings/account", label: "Account" }, ]; export function SidebarNav() { diff --git a/apps/web/src/app/(dashboard-legacy)/settings/account/page.tsx b/apps/web/src/app/(dashboard-legacy)/settings/account/page.tsx new file mode 100644 index 00000000000..159fe198d7a --- /dev/null +++ b/apps/web/src/app/(dashboard-legacy)/settings/account/page.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { authClient } from "@superset/auth/client"; +import { ACCOUNT_DELETION_GRACE_DAYS } from "@superset/shared/constants"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@superset/ui/alert-dialog"; +import { Button } from "@superset/ui/button"; +import { useMutation } from "@tanstack/react-query"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useTRPC } from "@/trpc/react"; + +export default function AccountSettingsPage() { + const trpc = useTRPC(); + const router = useRouter(); + const { data: session } = authClient.useSession(); + const user = session?.user; + const [isSigningOut, setIsSigningOut] = useState(false); + + const deleteAccount = useMutation( + trpc.user.deleteAccount.mutationOptions({ + onSuccess: async () => { + await authClient.signOut(); + router.replace("/sign-in"); + }, + }), + ); + + return ( +
+
+

Account

+ {user && ( +

+ {user.name} · {user.email} +

+ )} +
+ +
+
+
Sign out
+
+ +
+ +
+
+
Delete account
+ {deleteAccount.error && ( +
+ {deleteAccount.error.message} +
+ )} +
+ + + + + + + Delete account? + + All of your data will be permanently deleted after{" "} + {ACCOUNT_DELETION_GRACE_DAYS} days — sign back in before then to + restore your account. + + + + Cancel + deleteAccount.mutate()} + > + Delete account + + + + +
+
+ ); +} From 751c6ff88a91fd9f16a1da4da8827791db2a6db3 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 12 Aug 2026 16:22:04 -0700 Subject: [PATCH 5/7] fix(desktop): state member blast radius in org deletion confirm --- .../OrganizationSettings/OrganizationSettings.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx index 93dcebef184..a8593c14b9f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx @@ -589,9 +589,10 @@ export function OrganizationSettings({ Delete {organization.name}? - All of the organization's data will be permanently - deleted for every member — this cannot be undone. Type - the organization name to confirm. + {members.length > 1 + ? `All data will be permanently deleted for all ${members.length} members — this cannot be undone.` + : "All of the organization's data will be permanently deleted — this cannot be undone."}{" "} + Type the organization name to confirm. Date: Wed, 12 Aug 2026 17:26:59 -0700 Subject: [PATCH 6/7] fix(desktop): show deactivated members on the v2 org settings page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The v2 sidebar has no separate Members page — OrganizationSettings is the member-management surface, so it must opt into includeDeactivated and badge pending-deletion members, or their held seat is invisible. --- .../OrganizationSettings.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx index a8593c14b9f..96bb6f04b16 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx @@ -144,7 +144,7 @@ export function OrganizationSettings({ ); const { data: membersData, isPending: membersPending } = - cloudTrpc.organization.listMembers.useQuery(undefined); + cloudTrpc.organization.listMembers.useQuery({ includeDeactivated: true }); const members: TeamMember[] = useMemo(() => { if (!activeOrganizationId) return []; @@ -510,7 +510,13 @@ export function OrganizationSettings({ image={member.image} />
- + {member.name || "Unknown"} {isCurrentUserRow && ( @@ -521,6 +527,14 @@ export function OrganizationSettings({ You )} + {member.deletedAt && ( + + Deactivated + + )}
From 82fc4f9cf61458ebf0417b9ad2130a1d02d39034 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 12 Aug 2026 18:40:28 -0700 Subject: [PATCH 7/7] refactor(db,auth,trpc,mobile,desktop,web): rename users.deletedAt to deletionRequestedAt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The column marks when deletion was requested — the account is recoverable for the whole grace window and, in the tombstone design, no row is ever deleted at all. deletedAt is reserved for the terminal marker that ships with the tombstone purge. Avoids Slack's deleted-means-deactivated trap; migration regenerated (0066). --- .../PendingDeletionScreen/PendingDeletionScreen.tsx | 7 ++++--- .../src/renderer/routes/_authenticated/layout.tsx | 4 ++-- .../components/MembersSettings/MembersSettings.tsx | 6 +++--- .../routes/_authenticated/settings/members/types.ts | 2 +- .../OrganizationSettings/OrganizationSettings.tsx | 6 +++--- apps/electric-proxy/src/where.ts | 2 +- apps/mobile/screens/RootLayout/RootLayout.tsx | 2 +- .../AccountPendingDeletionScreen.tsx | 4 +++- .../hooks/useDaysUntilPurge/useDaysUntilPurge.ts | 9 ++++++--- apps/web/src/app/account-pending-deletion/page.tsx | 6 +++--- apps/web/src/proxy.ts | 6 +++++- packages/auth/src/server.ts | 13 ++++++++----- packages/db/drizzle/0066_add_user_deleted_at.sql | 1 - .../drizzle/0066_add_user_deletion_requested_at.sql | 1 + packages/db/drizzle/meta/0066_snapshot.json | 6 +++--- packages/db/drizzle/meta/_journal.json | 4 ++-- packages/db/src/schema/auth.ts | 2 +- packages/trpc/src/router/organization/members.ts | 2 +- .../trpc/src/router/organization/organization.ts | 4 ++-- packages/trpc/src/router/task/task.ts | 2 +- packages/trpc/src/router/user/user.ts | 13 ++++++++----- packages/trpc/src/trpc.ts | 2 +- 22 files changed, 60 insertions(+), 44 deletions(-) delete mode 100644 packages/db/drizzle/0066_add_user_deleted_at.sql create mode 100644 packages/db/drizzle/0066_add_user_deletion_requested_at.sql diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx index d2b441130ef..969511d5441 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx @@ -12,10 +12,10 @@ import { cloudTrpc } from "renderer/lib/cloud-trpc"; const DAY_MS = 24 * 60 * 60 * 1000; export function PendingDeletionScreen({ - deletedAt, + deletionRequestedAt, onReactivated, }: { - deletedAt: Date; + deletionRequestedAt: Date; onReactivated: () => void; }) { const navigate = useNavigate(); @@ -27,7 +27,8 @@ export function PendingDeletionScreen({ }); const purgeAt = - new Date(deletedAt).getTime() + ACCOUNT_DELETION_GRACE_DAYS * DAY_MS; + new Date(deletionRequestedAt).getTime() + + ACCOUNT_DELETION_GRACE_DAYS * DAY_MS; const daysRemaining = Math.max(0, Math.ceil((purgeAt - Date.now()) / DAY_MS)); return ( diff --git a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx index b4bd17e612b..c439ce1ec3a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/layout.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/layout.tsx @@ -263,10 +263,10 @@ function AuthenticatedLayout() { return signInRedirect; } - if (session?.user?.deletedAt) { + if (session?.user?.deletionRequestedAt) { return ( void refetch()} /> ); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsx index 2a91c14411c..1ad0aeca061 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsx @@ -60,7 +60,7 @@ export function MembersSettings({ visibleItems }: MembersSettingsProps) { name: m.user.name, email: m.user.email, image: m.user.image, - deletedAt: m.user.deletedAt, + deletionRequestedAt: m.user.deletionRequestedAt, })) .sort((a, b) => { const priorityDiff = @@ -162,7 +162,7 @@ export function MembersSettings({ visibleItems }: MembersSettingsProps) {
)} - {member.deletedAt && ( + {member.deletionRequestedAt && ( { const priorityDiff = @@ -512,7 +512,7 @@ export function OrganizationSettings({
)} - {member.deletedAt && ( + {member.deletionRequestedAt && ( ARRAY[$1::uuid] AND "deleted_at" IS NULL`; + const fragment = `"organization_ids" @> ARRAY[$1::uuid] AND "deletion_requested_at" IS NULL`; return { fragment, params: [organizationId] }; } diff --git a/apps/mobile/screens/RootLayout/RootLayout.tsx b/apps/mobile/screens/RootLayout/RootLayout.tsx index 95f85bffea3..ba29225dc20 100644 --- a/apps/mobile/screens/RootLayout/RootLayout.tsx +++ b/apps/mobile/screens/RootLayout/RootLayout.tsx @@ -19,7 +19,7 @@ export function RootLayout() { if (isPending) return null; - const pendingDeletion = !!session?.user.deletedAt; + const pendingDeletion = !!session?.user.deletionRequestedAt; return ( diff --git a/apps/mobile/screens/account-pending-deletion/AccountPendingDeletionScreen.tsx b/apps/mobile/screens/account-pending-deletion/AccountPendingDeletionScreen.tsx index d1a5ac0ef4d..07b5ad0a6e5 100644 --- a/apps/mobile/screens/account-pending-deletion/AccountPendingDeletionScreen.tsx +++ b/apps/mobile/screens/account-pending-deletion/AccountPendingDeletionScreen.tsx @@ -16,7 +16,9 @@ export function AccountPendingDeletionScreen() { const { signOut, isSigningOut } = useSignOut(); const [isReactivating, setIsReactivating] = useState(false); - const daysRemaining = useDaysUntilPurge(session?.user.deletedAt ?? null); + const daysRemaining = useDaysUntilPurge( + session?.user.deletionRequestedAt ?? null, + ); const handleReactivate = async () => { setIsReactivating(true); diff --git a/apps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/useDaysUntilPurge.ts b/apps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/useDaysUntilPurge.ts index 964ca3d4421..dc89c518106 100644 --- a/apps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/useDaysUntilPurge.ts +++ b/apps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/useDaysUntilPurge.ts @@ -2,8 +2,11 @@ import { ACCOUNT_DELETION_GRACE_DAYS } from "@superset/shared/constants"; const DAY_MS = 24 * 60 * 60 * 1000; -export function useDaysUntilPurge(deletedAt: Date | null): number | null { - if (!deletedAt) return null; - const purgeAt = deletedAt.getTime() + ACCOUNT_DELETION_GRACE_DAYS * DAY_MS; +export function useDaysUntilPurge( + deletionRequestedAt: Date | null, +): number | null { + if (!deletionRequestedAt) return null; + const purgeAt = + deletionRequestedAt.getTime() + ACCOUNT_DELETION_GRACE_DAYS * DAY_MS; return Math.max(0, Math.ceil((purgeAt - Date.now()) / DAY_MS)); } diff --git a/apps/web/src/app/account-pending-deletion/page.tsx b/apps/web/src/app/account-pending-deletion/page.tsx index 5f02bc81ffc..57754cf7f36 100644 --- a/apps/web/src/app/account-pending-deletion/page.tsx +++ b/apps/web/src/app/account-pending-deletion/page.tsx @@ -29,12 +29,12 @@ export default function AccountPendingDeletionPage() { }), ); - const deletedAt = session?.user.deletedAt; - const daysRemaining = deletedAt + const deletionRequestedAt = session?.user.deletionRequestedAt; + const daysRemaining = deletionRequestedAt ? Math.max( 0, Math.ceil( - (new Date(deletedAt).getTime() + + (new Date(deletionRequestedAt).getTime() + ACCOUNT_DELETION_GRACE_DAYS * DAY_MS - Date.now()) / DAY_MS, diff --git a/apps/web/src/proxy.ts b/apps/web/src/proxy.ts index 168e64e34ca..07385001f80 100644 --- a/apps/web/src/proxy.ts +++ b/apps/web/src/proxy.ts @@ -30,7 +30,11 @@ export default async function proxy(req: NextRequest) { // reachable so the reactivate mutation itself can go through. const isApiRoute = pathname.startsWith("/api") || pathname.startsWith("/trpc"); - if (session?.user.deletedAt && !isApiRoute && !isPublicRoute(pathname)) { + if ( + session?.user.deletionRequestedAt && + !isApiRoute && + !isPublicRoute(pathname) + ) { if (pathname !== "/account-pending-deletion") { return NextResponse.redirect( new URL("/account-pending-deletion", req.url), diff --git a/packages/auth/src/server.ts b/packages/auth/src/server.ts index b1639d61aa9..d7cbfff9965 100644 --- a/packages/auth/src/server.ts +++ b/packages/auth/src/server.ts @@ -53,11 +53,11 @@ const userOptions = { input: false, fieldName: "onboarded_at", }, - deletedAt: { + deletionRequestedAt: { type: "date", required: false, input: false, - fieldName: "deleted_at", + fieldName: "deletion_requested_at", }, }, } as const; @@ -146,7 +146,10 @@ export const auth = betterAuth({ return; } const session = await getSessionFromCtx(ctx); - if ((session?.user as { deletedAt?: Date | null })?.deletedAt) { + if ( + (session?.user as { deletionRequestedAt?: Date | null }) + ?.deletionRequestedAt + ) { throw new APIError("FORBIDDEN", { message: "Account is pending deletion.", }); @@ -888,14 +891,14 @@ export const auth = betterAuth({ // explicitly so the onboarding gate is deterministic. const userRow = await db.query.users.findFirst({ where: eq(authSchema.users.id, user.id), - columns: { onboardedAt: true, deletedAt: true }, + columns: { onboardedAt: true, deletionRequestedAt: true }, }); return { user: { ...user, onboardedAt: userRow?.onboardedAt ?? null, - deletedAt: userRow?.deletedAt ?? null, + deletionRequestedAt: userRow?.deletionRequestedAt ?? null, }, session: { ...session, diff --git a/packages/db/drizzle/0066_add_user_deleted_at.sql b/packages/db/drizzle/0066_add_user_deleted_at.sql deleted file mode 100644 index 8c220541401..00000000000 --- a/packages/db/drizzle/0066_add_user_deleted_at.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "auth"."users" ADD COLUMN "deleted_at" timestamp; \ No newline at end of file diff --git a/packages/db/drizzle/0066_add_user_deletion_requested_at.sql b/packages/db/drizzle/0066_add_user_deletion_requested_at.sql new file mode 100644 index 00000000000..486391cf332 --- /dev/null +++ b/packages/db/drizzle/0066_add_user_deletion_requested_at.sql @@ -0,0 +1 @@ +ALTER TABLE "auth"."users" ADD COLUMN "deletion_requested_at" timestamp; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0066_snapshot.json b/packages/db/drizzle/meta/0066_snapshot.json index fae9149ab5c..8c3d11caa5b 100644 --- a/packages/db/drizzle/meta/0066_snapshot.json +++ b/packages/db/drizzle/meta/0066_snapshot.json @@ -1,5 +1,5 @@ { - "id": "45460919-07c7-4389-b2dc-7a23788fe5d0", + "id": "0a720b09-14b6-4873-8e92-37dc6b5e7653", "prevId": "f3d09110-2b42-454a-a12b-f45c58fbcc31", "version": "7", "dialect": "postgresql", @@ -1827,8 +1827,8 @@ "primaryKey": false, "notNull": false }, - "deleted_at": { - "name": "deleted_at", + "deletion_requested_at": { + "name": "deletion_requested_at", "type": "timestamp", "primaryKey": false, "notNull": false diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index f1cc12cbcf2..0aa5420023c 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -467,8 +467,8 @@ { "idx": 66, "version": "7", - "when": 1786564882637, - "tag": "0066_add_user_deleted_at", + "when": 1786584902692, + "tag": "0066_add_user_deletion_requested_at", "breakpoints": true } ] diff --git a/packages/db/src/schema/auth.ts b/packages/db/src/schema/auth.ts index 266aa831f31..259e4066619 100644 --- a/packages/db/src/schema/auth.ts +++ b/packages/db/src/schema/auth.ts @@ -23,7 +23,7 @@ export const users = authSchema.table( image: text("image"), organizationIds: uuid("organization_ids").array().default([]).notNull(), onboardedAt: timestamp("onboarded_at"), - deletedAt: timestamp("deleted_at"), + deletionRequestedAt: timestamp("deletion_requested_at"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at") .defaultNow() diff --git a/packages/trpc/src/router/organization/members.ts b/packages/trpc/src/router/organization/members.ts index 793848ccbfa..5dd1e6ca22f 100644 --- a/packages/trpc/src/router/organization/members.ts +++ b/packages/trpc/src/router/organization/members.ts @@ -20,7 +20,7 @@ export const organizationMembersRouter = { const organizationId = await requireActiveOrgMembership(ctx); const conditions = [ eq(members.organizationId, organizationId), - isNull(users.deletedAt), + isNull(users.deletionRequestedAt), ]; if (input?.search) { const pattern = `%${input.search}%`; diff --git a/packages/trpc/src/router/organization/organization.ts b/packages/trpc/src/router/organization/organization.ts index 972f8d04402..3cc8eaa6b0f 100644 --- a/packages/trpc/src/router/organization/organization.ts +++ b/packages/trpc/src/router/organization/organization.ts @@ -87,7 +87,7 @@ export const organizationRouter = { const organizationId = await requireActiveOrgMembership(ctx); const conditions = [eq(members.organizationId, organizationId)]; if (!input?.includeDeactivated) { - conditions.push(isNull(users.deletedAt)); + conditions.push(isNull(users.deletionRequestedAt)); } return db .select({ @@ -100,7 +100,7 @@ export const organizationRouter = { name: users.name, email: users.email, image: users.image, - deletedAt: users.deletedAt, + deletionRequestedAt: users.deletionRequestedAt, }, }) .from(members) diff --git a/packages/trpc/src/router/task/task.ts b/packages/trpc/src/router/task/task.ts index 780ae38c289..4f581974126 100644 --- a/packages/trpc/src/router/task/task.ts +++ b/packages/trpc/src/router/task/task.ts @@ -163,7 +163,7 @@ async function getScopedAssigneeId( and( eq(members.organizationId, organizationId), eq(members.userId, assigneeId), - isNull(users.deletedAt), + isNull(users.deletionRequestedAt), ), ) .limit(1); diff --git a/packages/trpc/src/router/user/user.ts b/packages/trpc/src/router/user/user.ts index 5d8d6b44835..bbafc5d0289 100644 --- a/packages/trpc/src/router/user/user.ts +++ b/packages/trpc/src/router/user/user.ts @@ -102,7 +102,7 @@ export const userRouter = { await db .update(users) - .set({ deletedAt: new Date() }) + .set({ deletionRequestedAt: new Date() }) .where(eq(users.id, userId)); await db.delete(sessions).where(eq(sessions.userId, userId)); await db @@ -119,19 +119,22 @@ export const userRouter = { const user = await db.query.users.findFirst({ where: eq(users.id, userId), - columns: { deletedAt: true }, + columns: { deletionRequestedAt: true }, }); - if (!user?.deletedAt) return { success: true }; + if (!user?.deletionRequestedAt) return { success: true }; const graceMs = ACCOUNT_DELETION_GRACE_DAYS * 24 * 60 * 60 * 1000; - if (Date.now() - user.deletedAt.getTime() > graceMs) { + if (Date.now() - user.deletionRequestedAt.getTime() > graceMs) { throw new TRPCError({ code: "FORBIDDEN", message: "The recovery period has ended. Contact support@superset.sh.", }); } - await db.update(users).set({ deletedAt: null }).where(eq(users.id, userId)); + await db + .update(users) + .set({ deletionRequestedAt: null }) + .where(eq(users.id, userId)); return { success: true }; }), diff --git a/packages/trpc/src/trpc.ts b/packages/trpc/src/trpc.ts index bad1518bc31..0647e74ceb1 100644 --- a/packages/trpc/src/trpc.ts +++ b/packages/trpc/src/trpc.ts @@ -112,7 +112,7 @@ export const protectedProcedure = t.procedure }) .use(async ({ ctx, path, next }) => { if ( - ctx.session.user.deletedAt && + ctx.session.user.deletionRequestedAt && !PENDING_DELETION_ALLOWED_PROCEDURES.has(path) ) { throw new TRPCError({