diff --git a/apps/api/src/app/api/integrations/linear/callback/route.ts b/apps/api/src/app/api/integrations/linear/callback/route.ts index eb6e9a0ad39..7fe9d5f01d3 100644 --- a/apps/api/src/app/api/integrations/linear/callback/route.ts +++ b/apps/api/src/app/api/integrations/linear/callback/route.ts @@ -1,6 +1,7 @@ import { LinearClient } from "@linear/sdk"; import { db } from "@superset/db/client"; import { integrationConnections, members } from "@superset/db/schema"; +import { linearTokenResponseSchema } from "@superset/trpc/integrations/linear"; import { Client } from "@upstash/qstash"; import { and, eq } from "drizzle-orm"; @@ -73,8 +74,7 @@ export async function GET(request: Request) { ); } - const tokenData: { access_token: string; expires_in?: number } = - await tokenResponse.json(); + const tokenData = linearTokenResponseSchema.parse(await tokenResponse.json()); const linearClient = new LinearClient({ accessToken: tokenData.access_token, @@ -82,9 +82,7 @@ export async function GET(request: Request) { const viewer = await linearClient.viewer; const linearOrg = await viewer.organization; - const tokenExpiresAt = tokenData.expires_in - ? new Date(Date.now() + tokenData.expires_in * 1000) - : null; + const tokenExpiresAt = new Date(Date.now() + tokenData.expires_in * 1000); await db .insert(integrationConnections) @@ -93,6 +91,7 @@ export async function GET(request: Request) { connectedByUserId: userId, provider: "linear", accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, tokenExpiresAt, externalOrgId: linearOrg.id, externalOrgName: linearOrg.name, @@ -104,7 +103,10 @@ export async function GET(request: Request) { ], set: { accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, tokenExpiresAt, + disconnectedAt: null, + disconnectReason: null, externalOrgId: linearOrg.id, externalOrgName: linearOrg.name, connectedByUserId: userId, diff --git a/apps/api/src/app/api/integrations/linear/jobs/initial-sync/route.ts b/apps/api/src/app/api/integrations/linear/jobs/initial-sync/route.ts index d1fbc6a637e..11e4b9c89a8 100644 --- a/apps/api/src/app/api/integrations/linear/jobs/initial-sync/route.ts +++ b/apps/api/src/app/api/integrations/linear/jobs/initial-sync/route.ts @@ -1,12 +1,7 @@ -import { LinearClient } from "@linear/sdk"; +import type { LinearClient } from "@linear/sdk"; import { buildConflictUpdateColumns, db } from "@superset/db"; -import { - integrationConnections, - members, - taskStatuses, - tasks, - users, -} from "@superset/db/schema"; +import { members, taskStatuses, tasks, users } from "@superset/db/schema"; +import { getLinearClient } from "@superset/trpc/integrations/linear"; import { Receiver } from "@upstash/qstash"; import { and, eq, inArray, isNull } from "drizzle-orm"; import chunk from "lodash.chunk"; @@ -57,18 +52,14 @@ export async function POST(request: Request) { const { organizationId, creatorUserId } = parsed.data; - const connection = await db.query.integrationConnections.findFirst({ - where: and( - eq(integrationConnections.organizationId, organizationId), - eq(integrationConnections.provider, "linear"), - ), - }); - - if (!connection) { - return Response.json({ error: "No connection found", skipped: true }); + const client = await getLinearClient(organizationId); + if (!client) { + return Response.json({ + error: "No Linear connection or connection disconnected", + skipped: true, + }); } - const client = new LinearClient({ accessToken: connection.accessToken }); await performInitialSync(client, organizationId, creatorUserId); return Response.json({ success: true }); diff --git a/apps/api/src/app/api/integrations/linear/jobs/refresh-tokens/route.ts b/apps/api/src/app/api/integrations/linear/jobs/refresh-tokens/route.ts new file mode 100644 index 00000000000..377b80f4979 --- /dev/null +++ b/apps/api/src/app/api/integrations/linear/jobs/refresh-tokens/route.ts @@ -0,0 +1,79 @@ +import { db } from "@superset/db/client"; +import { integrationConnections } from "@superset/db/schema"; +import { refreshLinearToken } from "@superset/trpc/integrations/linear"; +import { Receiver } from "@upstash/qstash"; +import { and, eq, isNotNull, isNull, lt, sql } from "drizzle-orm"; +import { env } from "@/env"; + +const receiver = new Receiver({ + currentSigningKey: env.QSTASH_CURRENT_SIGNING_KEY, + nextSigningKey: env.QSTASH_NEXT_SIGNING_KEY, +}); + +export async function POST(request: Request) { + const body = await request.text(); + const signature = request.headers.get("upstash-signature"); + + const isDev = env.NODE_ENV === "development"; + + if (!isDev) { + if (!signature) { + return Response.json({ error: "Missing signature" }, { status: 401 }); + } + + try { + const isValid = await receiver.verify({ + body, + signature, + url: `${env.NEXT_PUBLIC_API_URL}/api/integrations/linear/jobs/refresh-tokens`, + }); + + if (!isValid) { + return Response.json({ error: "Invalid signature" }, { status: 401 }); + } + } catch (verifyError) { + console.error( + "[linear-refresh-cron] Signature verification failed:", + verifyError, + ); + return Response.json( + { error: "Signature verification failed" }, + { status: 401 }, + ); + } + } + + const stale = await db.query.integrationConnections.findMany({ + where: and( + eq(integrationConnections.provider, "linear"), + isNull(integrationConnections.disconnectedAt), + isNotNull(integrationConnections.refreshToken), + lt( + integrationConnections.tokenExpiresAt, + sql`now() + interval '90 minutes'`, + ), + ), + columns: { id: true }, + }); + + const results = await Promise.allSettled( + stale.map(async (connection) => { + try { + await refreshLinearToken(connection.id); + return { id: connection.id, ok: true }; + } catch (error) { + console.error( + `[linear-refresh-cron] failed for ${connection.id}:`, + error, + ); + return { id: connection.id, ok: false }; + } + }), + ); + + const succeeded = results.filter( + (result) => result.status === "fulfilled" && result.value.ok, + ).length; + + return Response.json({ candidates: stale.length, succeeded }); +} diff --git a/apps/web/src/app/(dashboard-legacy)/integrations/linear/components/ConnectionControls/ConnectionControls.tsx b/apps/web/src/app/(dashboard-legacy)/integrations/linear/components/ConnectionControls/ConnectionControls.tsx index 14fffb5fc1a..cd3807d437b 100644 --- a/apps/web/src/app/(dashboard-legacy)/integrations/linear/components/ConnectionControls/ConnectionControls.tsx +++ b/apps/web/src/app/(dashboard-legacy)/integrations/linear/components/ConnectionControls/ConnectionControls.tsx @@ -13,7 +13,7 @@ import { } from "@superset/ui/alert-dialog"; import { Button } from "@superset/ui/button"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { Unplug } from "lucide-react"; +import { AlertTriangle, Unplug } from "lucide-react"; import { useRouter } from "next/navigation"; import { env } from "@/env"; import { useTRPC } from "@/trpc/react"; @@ -21,11 +21,13 @@ import { useTRPC } from "@/trpc/react"; interface ConnectionControlsProps { organizationId: string; isConnected: boolean; + needsReconnect?: boolean; } export function ConnectionControls({ organizationId, isConnected, + needsReconnect = false, }: ConnectionControlsProps) { const trpc = useTRPC(); const router = useRouter(); @@ -52,6 +54,47 @@ export function ConnectionControls({ disconnectMutation.mutate({ organizationId }); }; + if (isConnected && needsReconnect) { + return ( +
+
+ +
Linear authorization expired. Reconnect to resume syncing.
+
+
+ + + + + + + + Disconnect Linear? + + This will remove the connection between your organization and + Linear. You can reconnect at any time. + + + + Cancel + + Disconnect + + + + +
+
+ ); + } + if (isConnected) { return ( diff --git a/apps/web/src/app/(dashboard-legacy)/integrations/linear/page.tsx b/apps/web/src/app/(dashboard-legacy)/integrations/linear/page.tsx index 445704871a6..b197e3c62ea 100644 --- a/apps/web/src/app/(dashboard-legacy)/integrations/linear/page.tsx +++ b/apps/web/src/app/(dashboard-legacy)/integrations/linear/page.tsx @@ -6,7 +6,7 @@ import { CardHeader, CardTitle, } from "@superset/ui/card"; -import { ArrowLeft, CheckCircle2 } from "lucide-react"; +import { AlertTriangle, ArrowLeft, CheckCircle2 } from "lucide-react"; import Link from "next/link"; import { SiLinear } from "react-icons/si"; import { api } from "@/trpc/server"; @@ -32,6 +32,7 @@ export default async function LinearIntegrationPage() { organizationId: organization.id, }); const isConnected = !!connection; + const needsReconnect = !!connection?.needsReconnect; return (
@@ -52,7 +53,12 @@ export default async function LinearIntegrationPage() {

Linear

- {isConnected ? ( + {needsReconnect ? ( + + + Reconnect required + + ) : isConnected ? ( Connected @@ -79,6 +85,7 @@ export default async function LinearIntegrationPage() { diff --git a/packages/db/drizzle/0042_linear_disconnect_state.sql b/packages/db/drizzle/0042_linear_disconnect_state.sql new file mode 100644 index 00000000000..78d4909cade --- /dev/null +++ b/packages/db/drizzle/0042_linear_disconnect_state.sql @@ -0,0 +1,2 @@ +ALTER TABLE "integration_connections" ADD COLUMN "disconnected_at" timestamp;--> statement-breakpoint +ALTER TABLE "integration_connections" ADD COLUMN "disconnect_reason" text; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0042_snapshot.json b/packages/db/drizzle/meta/0042_snapshot.json new file mode 100644 index 00000000000..b5ebca963f4 --- /dev/null +++ b/packages/db/drizzle/meta/0042_snapshot.json @@ -0,0 +1,5829 @@ +{ + "id": "1875cb9e-d6b5-4ba5-93fe-83a4f9fff27e", + "prevId": "526255a8-77d8-4a7d-9096-5b45619daaed", + "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 + } + }, + "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": {} + } + }, + "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 + } + }, + "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" + } + }, + "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 + } + }, + "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.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": "'{}'" + }, + "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": {}, + "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_config": { + "name": "agent_config", + "type": "jsonb", + "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": true + }, + "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" + }, + "automations_v2_project_id_v2_projects_id_fk": { + "name": "automations_v2_project_id_v2_projects_id_fk", + "tableFrom": "automations", + "tableTo": "v2_projects", + "columnsFrom": [ + "v2_project_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" + }, + "chat_sessions_v2_workspace_id_v2_workspaces_id_fk": { + "name": "chat_sessions_v2_workspace_id_v2_workspaces_id_fk", + "tableFrom": "chat_sessions", + "tableTo": "v2_workspaces", + "columnsFrom": [ + "v2_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "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_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.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 + }, + "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_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 + }, + "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 + }, + "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 + }, + "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_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_host_fk": { + "name": "v2_workspaces_host_fk", + "tableFrom": "v2_workspaces", + "tableTo": "v2_hosts", + "columnsFrom": [ + "organization_id", + "host_id" + ], + "columnsTo": [ + "organization_id", + "machine_id" + ], + "onDelete": "no action", + "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.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 073e250bff6..2f62e8121a7 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -295,6 +295,13 @@ "when": 1777766170049, "tag": "0041_add_automation_prompt_versions", "breakpoints": true + }, + { + "idx": 42, + "version": "7", + "when": 1777771854049, + "tag": "0042_linear_disconnect_state", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/schema.ts b/packages/db/src/schema/schema.ts index 3c36e200dd7..e2854b73969 100644 --- a/packages/db/src/schema/schema.ts +++ b/packages/db/src/schema/schema.ts @@ -189,6 +189,9 @@ export const integrationConnections = pgTable( refreshToken: text("refresh_token"), tokenExpiresAt: timestamp("token_expires_at"), + disconnectedAt: timestamp("disconnected_at"), + disconnectReason: text("disconnect_reason"), + externalOrgId: text("external_org_id"), externalOrgName: text("external_org_name"), diff --git a/packages/db/src/utils/sql.ts b/packages/db/src/utils/sql.ts index 95d8ecd58bf..aac2b02ceb9 100644 --- a/packages/db/src/utils/sql.ts +++ b/packages/db/src/utils/sql.ts @@ -1,5 +1,6 @@ import { getTableColumns, type SQL, sql } from "drizzle-orm"; import type { PgTable, PgTransaction } from "drizzle-orm/pg-core"; +import { dbWs } from "../client"; export function buildConflictUpdateColumns< T extends PgTable, @@ -31,3 +32,16 @@ export async function getCurrentTxid( return Number.parseInt(txid, 10); } + +export async function withConnectionLock( + connectionId: string, + // biome-ignore lint/suspicious/noExplicitAny: Transaction type varies by client (Neon, PostgresJs, etc) + fn: (tx: PgTransaction) => Promise, +): Promise { + return dbWs.transaction(async (tx) => { + await tx.execute( + sql`SELECT pg_advisory_xact_lock(hashtextextended(${connectionId}::text, 0))`, + ); + return fn(tx); + }); +} diff --git a/packages/trpc/src/env.ts b/packages/trpc/src/env.ts index e9564c7180a..8d7e9d43850 100644 --- a/packages/trpc/src/env.ts +++ b/packages/trpc/src/env.ts @@ -25,6 +25,8 @@ export const env = createEnv({ SECRETS_ENCRYPTION_KEY: z.string().min(1), ANTHROPIC_API_KEY: z.string(), RELAY_URL: z.string().url(), + LINEAR_CLIENT_ID: z.string().min(1), + LINEAR_CLIENT_SECRET: z.string().min(1), }, clientPrefix: "PUBLIC_", client: {}, diff --git a/packages/trpc/src/lib/integrations/linear/index.ts b/packages/trpc/src/lib/integrations/linear/index.ts index 85a920a08ad..d6265ea9997 100644 --- a/packages/trpc/src/lib/integrations/linear/index.ts +++ b/packages/trpc/src/lib/integrations/linear/index.ts @@ -1,3 +1,10 @@ +export { + callLinear, + isLinearAuthError, + type LinearTokenResponse, + linearTokenResponseSchema, + refreshLinearToken, +} from "../../../router/integration/linear/refresh"; export { getLinearClient, mapPriorityFromLinear, diff --git a/packages/trpc/src/router/integration/linear/constants.ts b/packages/trpc/src/router/integration/linear/constants.ts new file mode 100644 index 00000000000..d20121e87e7 --- /dev/null +++ b/packages/trpc/src/router/integration/linear/constants.ts @@ -0,0 +1,3 @@ +export const REFRESH_BUFFER_MS = 5 * 60 * 1000; + +export const REFRESH_TOKEN_TIMEOUT_MS = 10 * 1000; diff --git a/packages/trpc/src/router/integration/linear/linear.ts b/packages/trpc/src/router/integration/linear/linear.ts index 6732896c72a..2e37d53b56c 100644 --- a/packages/trpc/src/router/integration/linear/linear.ts +++ b/packages/trpc/src/router/integration/linear/linear.ts @@ -11,7 +11,7 @@ import { and, eq } from "drizzle-orm"; import { z } from "zod"; import { protectedProcedure } from "../../../trpc"; import { verifyOrgAdmin, verifyOrgMembership } from "../utils"; -import { getLinearClient } from "./utils"; +import { callLinear } from "./refresh"; export const linearRouter = { getConnection: protectedProcedure @@ -23,10 +23,19 @@ export const linearRouter = { eq(integrationConnections.organizationId, input.organizationId), eq(integrationConnections.provider, "linear"), ), - columns: { id: true, config: true }, + columns: { + id: true, + config: true, + disconnectedAt: true, + disconnectReason: true, + }, }); if (!connection) return null; - return { config: connection.config as LinearConfig | null }; + return { + config: connection.config as LinearConfig | null, + needsReconnect: !!connection.disconnectedAt, + disconnectReason: connection.disconnectReason, + }; }), disconnect: protectedProcedure @@ -34,12 +43,9 @@ export const linearRouter = { .mutation(async ({ ctx, input }) => { await verifyOrgAdmin(ctx.session.user.id, input.organizationId); - const client = await getLinearClient(input.organizationId); - if (client) { - try { - await client.logout(); - } catch {} - } + try { + await callLinear(input.organizationId, (client) => client.logout()); + } catch {} const result = await dbWs.transaction(async (tx) => { // 1. Delete Linear-synced tasks @@ -122,9 +128,10 @@ export const linearRouter = { .input(z.object({ organizationId: z.uuid() })) .query(async ({ ctx, input }) => { await verifyOrgMembership(ctx.session.user.id, input.organizationId); - const client = await getLinearClient(input.organizationId); - if (!client) return []; - const teams = await client.teams(); + const teams = await callLinear(input.organizationId, (client) => + client.teams(), + ); + if (!teams) return []; return teams.nodes.map((t) => ({ id: t.id, name: t.name, key: t.key })); }), diff --git a/packages/trpc/src/router/integration/linear/refresh.ts b/packages/trpc/src/router/integration/linear/refresh.ts new file mode 100644 index 00000000000..dc8a017295c --- /dev/null +++ b/packages/trpc/src/router/integration/linear/refresh.ts @@ -0,0 +1,162 @@ +import { LinearClient } from "@linear/sdk"; +import { db } from "@superset/db/client"; +import { integrationConnections } from "@superset/db/schema"; +import { withConnectionLock } from "@superset/db/utils"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod"; +import { env } from "../../../env"; +import { REFRESH_BUFFER_MS, REFRESH_TOKEN_TIMEOUT_MS } from "./constants"; +import { getLinearClient, markConnectionDisconnected } from "./utils"; + +export const linearTokenResponseSchema = z.object({ + access_token: z.string(), + refresh_token: z.string(), + expires_in: z.number(), + token_type: z.string().optional(), + scope: z.string().optional(), +}); + +export type LinearTokenResponse = z.infer; + +type RefreshResult = + | { disconnected: true } + | { disconnected: false; accessToken: string }; + +export async function refreshLinearToken( + connectionId: string, +): Promise { + return withConnectionLock(connectionId, async (tx) => { + const [connection] = await tx + .select({ + accessToken: integrationConnections.accessToken, + refreshToken: integrationConnections.refreshToken, + tokenExpiresAt: integrationConnections.tokenExpiresAt, + disconnectedAt: integrationConnections.disconnectedAt, + }) + .from(integrationConnections) + .where(eq(integrationConnections.id, connectionId)) + .limit(1); + + if (!connection?.refreshToken) return { disconnected: true }; + if (connection.disconnectedAt) return { disconnected: true }; + + if ( + connection.tokenExpiresAt && + connection.tokenExpiresAt.getTime() > Date.now() + REFRESH_BUFFER_MS + ) { + return { disconnected: false, accessToken: connection.accessToken }; + } + + const controller = new AbortController(); + const timeout = setTimeout( + () => controller.abort(), + REFRESH_TOKEN_TIMEOUT_MS, + ); + let response: Response; + try { + response = await fetch("https://api.linear.app/oauth/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + signal: controller.signal, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: connection.refreshToken, + client_id: env.LINEAR_CLIENT_ID, + client_secret: env.LINEAR_CLIENT_SECRET, + }), + }); + } finally { + clearTimeout(timeout); + } + + if (!response.ok) { + const body = (await response.json().catch(() => ({}))) as { + error?: string; + }; + if (body?.error === "invalid_grant") { + await tx + .update(integrationConnections) + .set({ + disconnectedAt: new Date(), + disconnectReason: "invalid_grant", + }) + .where(eq(integrationConnections.id, connectionId)); + return { disconnected: true }; + } + throw new Error( + `Linear token refresh failed: ${response.status} ${response.statusText}`, + ); + } + + const data = linearTokenResponseSchema.parse(await response.json()); + const tokenExpiresAt = new Date(Date.now() + data.expires_in * 1000); + + await tx + .update(integrationConnections) + .set({ + accessToken: data.access_token, + refreshToken: data.refresh_token, + tokenExpiresAt, + disconnectedAt: null, + disconnectReason: null, + }) + .where(eq(integrationConnections.id, connectionId)); + + return { disconnected: false, accessToken: data.access_token }; + }); +} + +export async function callLinear( + organizationId: string, + fn: (client: LinearClient) => Promise, +): Promise { + const client = await getLinearClient(organizationId); + if (!client) return null; + + try { + return await fn(client); + } catch (error) { + if (!isLinearAuthError(error)) throw error; + + const connection = await db.query.integrationConnections.findFirst({ + where: and( + eq(integrationConnections.organizationId, organizationId), + eq(integrationConnections.provider, "linear"), + ), + }); + if (!connection) return null; + if (!connection.refreshToken) { + await markConnectionDisconnected(connection.id, "no_refresh_token"); + return null; + } + + const result = await refreshLinearToken(connection.id); + if (result.disconnected) return null; + + try { + return await fn(new LinearClient({ accessToken: result.accessToken })); + } catch (retryError) { + if (isLinearAuthError(retryError)) return null; + throw retryError; + } + } +} + +export function isLinearAuthError(error: unknown): boolean { + if (typeof error !== "object" || error === null) return false; + const candidate = error as { + type?: string; + errors?: Array<{ extensions?: { code?: string } }>; + status?: number; + }; + if (candidate.type === "AuthenticationError") return true; + if (candidate.status === 401) return true; + if ( + candidate.errors?.some( + (item) => item.extensions?.code === "AUTHENTICATION_ERROR", + ) + ) { + return true; + } + return false; +} diff --git a/packages/trpc/src/router/integration/linear/utils.ts b/packages/trpc/src/router/integration/linear/utils.ts index 034954c93e0..b9945fdeebb 100644 --- a/packages/trpc/src/router/integration/linear/utils.ts +++ b/packages/trpc/src/router/integration/linear/utils.ts @@ -2,6 +2,8 @@ import { LinearClient } from "@linear/sdk"; import { db } from "@superset/db/client"; import { integrationConnections } from "@superset/db/schema"; import { and, eq } from "drizzle-orm"; +import { REFRESH_BUFFER_MS } from "./constants"; +import { isLinearAuthError, refreshLinearToken } from "./refresh"; type Priority = "urgent" | "high" | "medium" | "low" | "none"; @@ -45,9 +47,43 @@ export async function getLinearClient( ), }); - if (!connection) { + if (!connection || connection.disconnectedAt) { return null; } + const expiresSoon = + connection.tokenExpiresAt && + connection.tokenExpiresAt.getTime() - Date.now() < REFRESH_BUFFER_MS; + + if (expiresSoon) { + if (!connection.refreshToken) { + await markConnectionDisconnected(connection.id, "no_refresh_token"); + return null; + } + try { + const result = await refreshLinearToken(connection.id); + if (result.disconnected) return null; + return new LinearClient({ accessToken: result.accessToken }); + } catch (error) { + const tokenStillValid = + connection.tokenExpiresAt && + connection.tokenExpiresAt.getTime() > Date.now(); + if (tokenStillValid && !isLinearAuthError(error)) { + return new LinearClient({ accessToken: connection.accessToken }); + } + throw error; + } + } + return new LinearClient({ accessToken: connection.accessToken }); } + +export async function markConnectionDisconnected( + connectionId: string, + reason: string, +): Promise { + await db + .update(integrationConnections) + .set({ disconnectedAt: new Date(), disconnectReason: reason }) + .where(eq(integrationConnections.id, connectionId)); +} diff --git a/plans/20260501-linear-team-entity.md b/plans/20260501-linear-team-entity.md new file mode 100644 index 00000000000..216a803ee2f --- /dev/null +++ b/plans/20260501-linear-team-entity.md @@ -0,0 +1,736 @@ +# Linear integration overhaul: teams entity, per-team numbering, OAuth refresh, app-actor + +Three workstreams bundled because they all touch the Linear integration code and read better as one cohesive design: + +1. **Teams entity + per-team task numbering** — replaces the inconsistent `tasks.slug` column with stable `{teamKey}-{number}` identifiers backed by a teams table with an explicit linkage to Linear. +2. **OAuth token refresh** — fixes silent 401-after-24-hours that's currently breaking connections. Linear migrated to short-lived tokens on 2026-04-01; our code was written for the old long-lived model and was never updated. +3. **`actor=app` switch + connect/error UX** — preparation for submitting Superset to Linear's integration directory. Bundles cleanly here since we're already touching the connect route. + +Ship order favours user-visible urgency: **OAuth refresh first** (workstream 2), then **teams + numbering + actor switch + UX** (workstreams 1 + 3 together, since they share files). + +--- + +## Workstream 1: Teams entity + per-team numbering + +### Context + +`tasks.slug` is text + `unique(organizationId, slug)`. Two writers populate it inconsistently: + +- **Local creation** (`packages/trpc/src/router/task/task.ts:207-220` via `generateBaseTaskSlug`/`generateUniqueTaskSlug` in `packages/shared/src/task-slug.ts`) → kebab-case-from-title with numeric suffix on collision. Agents produce 30+ char nonsense slugs. +- **Linear sync** (`apps/api/.../sync-task/route.ts:217`, `apps/api/.../initial-sync/utils.ts:183`, `apps/api/.../webhook/route.ts:173`) → overwrites `slug` with Linear's `issue.identifier` (`SUPER-237`). + +Same column carries two semantically different things. Result: hybrid identifier space, hard to predict, hard to reference. + +### Goals + +- Replace `tasks.slug` with a stable, human-readable identifier in the form `{teamKey}-{number}` (e.g. `SUPER-103`). +- Per-team monotonic numbering allocated atomically. +- Identifier is canonical for both local-only and Linear-synced tasks. Linear's identifier (`ENG-42`) becomes metadata on `external_key`. +- Renaming a team's key keeps old links working via redirect. +- Linear teams link to our teams via an explicit admin-set linkage (one of our teams ↔ one Linear team). Issues from non-linked Linear teams are ignored. +- One default team per org for now; multi-team UI deferred. + +### Non-goals (this workstream) + +- Auto-mirroring Linear teams 1:1 in our data model. Linkage is admin-driven via a UI dropdown, not auto-discovered from webhooks/sync. +- Auto-detecting Linear team-key renames. Linear emits no Team webhook events; opportunistic sync via Issue payloads is deferred. +- Surfacing teams as a multi-team UI in org settings. One default team per org, configurable Linear-link only. + +### Schema + +#### `teams` + +Stable team identity. No `key` column — keys are temporal and live in `team_keys`. Carries the Linear linkage directly, mirroring the `external_provider/id/key` pattern already used on `tasks` and `task_statuses`. + +```ts +export const teams = pgTable("teams", { + id: uuid().primaryKey().defaultRandom(), + organizationId: uuid("organization_id").notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + name: text().notNull(), + archivedAt: timestamp("archived_at"), + + // Linkage to an external integration's team (Linear team UUID). + // Set via the integrations UI dropdown. Null = unlinked, no external sync. + externalProvider: integrationProvider("external_provider"), + externalId: text("external_id"), // Linear team UUID + externalKey: text("external_key"), // Linear's team key, e.g. "ENG" — denormalized for display + + createdAt: timestamp("created_at").notNull().defaultNow(), + updatedAt: timestamp("updated_at").notNull().defaultNow().$onUpdate(() => new Date()), +}, (t) => [ + index("teams_organization_id_idx").on(t.organizationId), + unique("teams_org_external_unique").on(t.organizationId, t.externalProvider, t.externalId), +]); +``` + +`teams.externalKey` is Linear's team key (`ENG`) — distinct from `team_keys.key` (our team's identifier prefix, e.g. `SUPER`). They're independent: an admin can link our `SUPER` team to Linear's `ENG` team, and tasks in our team get identifiers like `SUPER-103` in our app and `ENG-42` in Linear, with `external_key` on the task storing `ENG-42`. + +#### `team_keys` + +Lifecycle of every key a team has ever used. Current key = `retired_at IS NULL`. Resolution of `SUPER-103` and `OLDPREFIX-103` (after a rename) both hit this table — no UNION across "current" and "history." + +```ts +export const teamKeys = pgTable("team_keys", { + id: uuid().primaryKey().defaultRandom(), + teamId: uuid("team_id").notNull().references(() => teams.id, { onDelete: "cascade" }), + organizationId: uuid("organization_id").notNull() + .references(() => organizations.id, { onDelete: "cascade" }), + key: text().notNull(), + effectiveAt: timestamp("effective_at").notNull().defaultNow(), + retiredAt: timestamp("retired_at"), +}, (t) => [ + unique("team_keys_org_key_unique").on(t.organizationId, t.key), + uniqueIndex("team_keys_team_id_current_unique") + .on(t.teamId) + .where(sql`${t.retiredAt} IS NULL`), + index("team_keys_team_id_idx").on(t.teamId), +]); +``` + +Full `unique(organization_id, key)` (not partial): a key, once used in an org, is reserved forever. Prevents teamA renaming away from `FOO`, teamB later claiming `FOO`, and `FOO-7` becoming ambiguous. + +#### `team_sequences` + +Atomic per-team counter. One row per team. Separate table — keeps hot counter updates off the teams entity row. + +```ts +export const teamSequences = pgTable("team_sequences", { + teamId: uuid("team_id").primaryKey() + .references(() => teams.id, { onDelete: "cascade" }), + lastNumber: integer("last_number").notNull().default(0), +}); +``` + +Allocation is one statement, atomic via row-level X-lock: + +```ts +const [{ number }] = await tx + .insert(teamSequences) + .values({ teamId, lastNumber: 1 }) + .onConflictDoUpdate({ + target: teamSequences.teamId, + set: { lastNumber: sql`${teamSequences.lastNumber} + 1` }, + }) + .returning({ number: teamSequences.lastNumber }); +``` + +Surrounding tx rollback unwinds the counter — no gaps from failed inserts. (Postgres native sequences advance on rollback; row UPDATE is what we want here.) + +#### `tasks` changes + +Add `team_id` (FK), `number` (integer). Drop `slug` after one release. Keep `external_key` as Linear metadata. + +```ts +{ + // … existing columns … + teamId: uuid("team_id").notNull().references(() => teams.id, { onDelete: "restrict" }), + number: integer().notNull(), +} +// indexes / constraints: +// unique("tasks_team_number_unique").on(team_id, number) +// index("tasks_team_id_idx").on(team_id) +// partial unique on (organization_id, external_key) where external_key IS NOT NULL +// keep tasks_external_unique(organization_id, external_provider, external_id) +// drop tasks_org_slug_unique, tasks_slug_idx (after slug column drop) +``` + +`onDelete: "restrict"` on `team_id`: a task can't dangle without a team. Org delete still cascades through teams → tasks. + +Partial unique on `external_key` lets us resolve `@task:ENG-42` mentions to a single task (see Read paths). + +### Migration + +Single Drizzle migration plus one deploy-time script. Backfill is uniform — every org gets one team, every task flattens into that team's number space. + +```sql +-- 1. DDL: create teams, team_keys, team_sequences (per definitions above). + +-- 2. For each org with any tasks, create a default team. +INSERT INTO teams (id, organization_id, name) +SELECT gen_random_uuid(), o.id, o.name +FROM auth.organizations o +WHERE EXISTS (SELECT 1 FROM tasks t WHERE t.organization_id = o.id); + +-- 3. (TS deploy script) Insert the initial team_keys row for each new team. +-- rawKey = upper(replace(org.slug, /[^A-Z0-9]/g, '')) +-- key = rawKey.length > 0 ? rawKey : 'TASK' +-- INSERT INTO team_keys (team_id, organization_id, key) VALUES (...) + +-- 4. (TS deploy script) For each org with a Linear connection AND a non-null +-- linearConfig.newTasksTeamId, populate the team's external linkage: +-- a) call client.team(newTasksTeamId) to get { id, key, name } +-- b) UPDATE teams SET external_provider='linear', external_id=$id, external_key=$key +-- WHERE id = $defaultTeamId +-- Orgs without newTasksTeamId set: leave unlinked, surface a "Link Linear team" +-- prompt next time they visit integrations page. + +-- 5. Set tasks.team_id and tasks.number — flatten everything into the org's default team. +WITH numbered AS ( + SELECT t.id, + (SELECT id FROM teams tm WHERE tm.organization_id = t.organization_id LIMIT 1) AS team_id, + ROW_NUMBER() OVER (PARTITION BY t.organization_id ORDER BY t.created_at, t.id) AS num + FROM tasks t +) +UPDATE tasks SET team_id = numbered.team_id, number = numbered.num +FROM numbered WHERE tasks.id = numbered.id; + +-- 6. Seed team_sequences. +INSERT INTO team_sequences (team_id, last_number) +SELECT team_id, COALESCE(MAX(number), 0) FROM tasks GROUP BY team_id; + +-- 7. NOT NULL + unique on tasks. +ALTER TABLE tasks ALTER COLUMN team_id SET NOT NULL; +ALTER TABLE tasks ALTER COLUMN number SET NOT NULL; +ALTER TABLE tasks ADD CONSTRAINT tasks_team_number_unique UNIQUE (team_id, number); + +-- 8. Partial unique on external_key for mention-fallback resolution. +CREATE UNIQUE INDEX tasks_org_external_key_unique + ON tasks (organization_id, external_key) + WHERE external_key IS NOT NULL; + +-- 9. Keep slug column + tasks_org_slug_unique for one release. +-- Dual-write `${currentTeamKey}-${number}` so shipped CLI/renderer keep working. +-- Drop in a follow-up migration after SDK consumers migrate. +``` + +Backfill notes: + +- **Linear-synced tasks lose their Linear-shaped identifier as the canonical key.** A task that was `ENG-42` in our slug column gets renumbered to (e.g.) `SUPER-103`. The Linear identifier is preserved in `external_key`. UI surfaces both as `SUPER-103 · ENG-42`. +- **Pre-existing tasks from non-linked Linear teams stay in our DB but stop receiving updates.** They become orphans. Surface as a one-time notification to admins ("X issues from Linear team `DESIGN` are no longer syncing — keep or delete?"). The actual cleanup UI is a follow-up. +- **Org-slug-derived team key**: empty/non-alphanumeric slugs fall back to `TASK`. The deploy script handles regex sanitization; SQL alone would be ugly. + +### Read paths + +#### Identifier resolution + +`task.byIdOrKey` (renamed from `byIdOrSlug`) accepts a UUID or a key like `SUPER-103`: + +``` +input = "SUPER-103" or UUID + +1. UUID? → tasks.id lookup. +2. Match /^([A-Za-z][A-Za-z0-9]*)-(\d+)$/i: + a. SELECT t.* FROM tasks t + JOIN team_keys tk ON tk.team_id = t.team_id + WHERE tk.organization_id = $org AND tk.key = $prefix AND t.number = $number; + → if hit and tk.retired_at IS NULL, return. + → if hit and tk.retired_at IS NOT NULL, return with redirected: true plus + the canonical identifier. + b. If no match, fallback: SELECT * FROM tasks + WHERE organization_id = $org AND external_key = $input; + → handles old `@task:ENG-42` mentions where ENG-42 is Linear's identifier. +3. Else: not found. +``` + +Single query for the common case. `team_keys` consulted whether the matched key is current or retired — no UNION. + +URL `/tasks/$taskId`: same logic. On redirect (`tk.retired_at IS NOT NULL`), client calls `navigate({ replace: true })` to the canonical key. + +#### Display projection + +```ts +db.select({ + task: tasks, + teamKey: teamKeys.key, +}) +.from(tasks) +.innerJoin(teamKeys, and( + eq(teamKeys.teamId, tasks.teamId), + isNull(teamKeys.retiredAt), +)) +``` + +`identifier = teamKey + '-' + task.number`. Computed in the projection step, not stored. Ship as `Task.identifier` on the SDK and in tRPC return shapes. `Task.slug` stays for one release as a deprecated alias = `identifier`. + +### Write paths + +#### Local task creation + +`packages/trpc/src/router/task/task.ts` (`createTask`): + +```ts +async function createTask(ctx, input) { + const organizationId = await requireActiveOrgMembership(ctx); + + return dbWs.transaction(async (tx) => { + const teamId = await resolveDefaultTeam(tx, organizationId); + const statusId = input.statusId + ? await getScopedStatusId(tx, organizationId, input.statusId, ...) + : await seedDefaultStatuses(organizationId, tx); + const assigneeId = input.assigneeId + ? await getScopedAssigneeId(tx, organizationId, input.assigneeId, ...) + : null; + + const [{ number }] = await tx + .insert(teamSequences) + .values({ teamId, lastNumber: 1 }) + .onConflictDoUpdate({ + target: teamSequences.teamId, + set: { lastNumber: sql`${teamSequences.lastNumber} + 1` }, + }) + .returning({ number: teamSequences.lastNumber }); + + const [task] = await tx.insert(tasks).values({ + organizationId, teamId, number, ...input, + }).returning(); + + const txid = await getCurrentTxid(tx); + return { task, txid }; + }).then(async (result) => { + if (result.task) syncTask(result.task.id); + return result; + }); +} +``` + +Deleted: +- `packages/shared/src/task-slug.ts` (entire file + test) +- `TASK_SLUG_RETRY_LIMIT` retry loop and `isConstraintError` helper +- Pre-insert `existingSlugs` SELECT + +`resolveDefaultTeam(tx, organizationId)`: +- Query for an existing non-archived team in the org. +- If none, INSERT one + initial `team_keys` row + `team_sequences` row, all in tx. +- Returns the team UUID. + +Lazy creation keeps orgs without tasks from getting empty default teams. + +#### Linear sync — outbound (local task → Linear issue) + +`apps/api/.../sync-task/route.ts`: + +The local task already has its canonical identifier (`SUPER-103`) from creation. The QStash job pushes it to the **linked Linear team** and writes the Linear identifier back into `external_key`. **No change to `team_id` or `number` after the Linear call.** Our identifier is stable; Linear's is metadata. + +```ts +const task = await db.query.tasks.findFirst({ + where: eq(tasks.id, taskId), + with: { team: true }, +}); + +if (task.team.externalProvider !== "linear" || !task.team.externalId) { + // Task's team isn't linked to Linear — outbound sync is a no-op + return; +} + +// push to Linear using task.team.externalId as teamId +// on success: +await db.update(tasks).set({ + externalProvider: "linear", + externalId: issue.id, + externalKey: issue.identifier, + externalUrl: issue.url, + lastSyncedAt: new Date(), + syncError: null, +}).where(eq(tasks.id, task.id)); +``` + +Drop the `slug: issue.identifier` line from the existing code. `linearConfig.newTasksTeamId` becomes redundant — the linked team IS the target. + +#### Linear sync — inbound (Linear webhook → our task) + +`apps/api/.../webhook/route.ts`: + +Filter inbound by linkage. Issues from Linear teams not linked to any Superset team are skipped: + +```ts +const linkedTeam = await db.query.teams.findFirst({ + where: and( + eq(teams.organizationId, connection.organizationId), + eq(teams.externalProvider, "linear"), + eq(teams.externalId, payload.data.team.id), + ), +}); + +if (!linkedTeam) { + await markEventSkipped(webhookEvent.id, "team_not_linked"); + return Response.json({ success: true, skipped: true }); +} + +const [{ number }] = /* same atomic increment, scoped to linkedTeam.id */; + +await tx.insert(tasks).values({ + organizationId: connection.organizationId, + teamId: linkedTeam.id, + number, + title: issue.title, + // … other fields … + externalProvider: "linear", + externalId: issue.id, + externalKey: issue.identifier, + externalUrl: issue.url, +}).onConflictDoUpdate({ + target: [tasks.organizationId, tasks.externalProvider, tasks.externalId], + set: { /* same fields, BUT do NOT change team_id or number on conflict */ }, +}); +``` + +Critical: the `onConflictDoUpdate.set` clause must NOT touch `team_id` or `number`. Once a task has them, they're stable for life. Re-running the webhook is idempotent for identifier. + +#### Initial sync + +`apps/api/.../initial-sync/route.ts`: + +`syncWorkflowStates` loop is unchanged — that handles `taskStatuses`. For tasks: only fetch issues for the linked Linear team(s): + +```ts +const linkedTeams = await db.query.teams.findMany({ + where: and(eq(teams.organizationId, organizationId), eq(teams.externalProvider, "linear")), +}); + +for (const ourTeam of linkedTeams) { + const issues = await fetchIssuesForTeam(client, ourTeam.externalId); + // map and insert with teamId: ourTeam.id, batched number allocation +} +``` + +`mapIssueToTask` (`apps/api/.../initial-sync/utils.ts:154`) drops `slug: issue.identifier`. Tasks are inserted without a number; the loop assigns numbers from the team sequence in batches: + +```ts +const [{ lastNumber: end }] = await tx + .insert(teamSequences) + .values({ teamId: ourTeam.id, lastNumber: issues.length }) + .onConflictDoUpdate({ + target: teamSequences.teamId, + set: { lastNumber: sql`${teamSequences.lastNumber} + ${issues.length}` }, + }) + .returning({ lastNumber: teamSequences.lastNumber }); +const start = end - issues.length + 1; +// issues[i] gets number = start + i +``` + +One round-trip for the whole batch. + +#### Linear disconnect + +`packages/trpc/src/router/integration/linear/linear.ts:32-119`: + +Today: deletes `tasks WHERE externalProvider='linear'` and `taskStatuses WHERE externalProvider='linear'`, remaps statuses, deletes the connection. + +Add: clear the team's external linkage (`UPDATE teams SET external_provider=NULL, external_id=NULL, external_key=NULL`) but keep the team and its keys. The org's default team and its number sequence persist regardless of integration state. Linear-synced tasks are still deleted; their numbers are not reused (matches Linear's own behavior re: deleted issue numbers). + +### Mention/search fallback for `external_key` + +Pre-migration `@task:ENG-42` mentions worked because `slug = 'ENG-42'`. Post-migration, `ENG-42` no longer matches `team_keys` (the team's key is `SUPER`). + +Resolution falls back to `external_key` (step 2b in the resolver). Partial unique index `(organization_id, external_key) WHERE external_key IS NOT NULL` guarantees uniqueness. + +UI display of Linear-synced tasks shows both: `SUPER-103 · ENG-42` (canonical · external). Search indexes both. + +--- + +## Workstream 2: OAuth token refresh (urgent) + +### What's broken + +Linear migrated all OAuth apps to short-lived (24h) access tokens with rotating refresh tokens on **2026-04-01**. Our code was written for the old long-lived model and was never updated. Specifically: + +1. **Refresh token never stored.** `apps/api/.../linear/callback/route.ts:76-77` types the response as `{ access_token, expires_in? }` — `refresh_token` isn't even read. `integrationConnections.refreshToken` column exists in the schema (`packages/db/src/schema/schema.ts:188`) but is never populated for Linear. +2. **Expiration never checked.** `getLinearClient` (`packages/trpc/src/router/integration/linear/utils.ts:38-53`) reads the row and constructs `new LinearClient({ accessToken: connection.accessToken })`. Doesn't look at `tokenExpiresAt`. Doesn't refresh. +3. **No refresh logic anywhere.** +4. **No connection-level error state.** When a token 401s, the error gets written to per-task `syncError`. The connection row still says "Connected." UI gives no signal. + +Result: any connection re-authed since 2026-04-01 silently breaks within 24h. This matches the symptoms users are reporting. + +### Fix + +#### Schema + +Add a connection-broken signal so the UI can surface "Reconnect Linear": + +```ts +// integrationConnections — add: +disconnectedAt: timestamp("disconnected_at"), // set when refresh returns invalid_grant or admin disconnects +disconnectReason: text("disconnect_reason"), // "invalid_grant" | "user_revoked" | "admin_disconnected" +``` + +#### Callback writes the full token triple + +`apps/api/.../linear/callback/route.ts`: + +```ts +const tokenData: { + access_token: string; + refresh_token: string; + expires_in: number; + token_type: string; + scope: string; +} = await tokenResponse.json(); + +const tokenExpiresAt = new Date(Date.now() + tokenData.expires_in * 1000); + +await db.insert(integrationConnections).values({ + // … existing fields … + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, // NEW — was never stored + tokenExpiresAt, + disconnectedAt: null, + disconnectReason: null, +}).onConflictDoUpdate({ + target: [integrationConnections.organizationId, integrationConnections.provider], + set: { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + tokenExpiresAt, + disconnectedAt: null, + disconnectReason: null, + // … etc + }, +}); +``` + +#### Refresh helper (single-flight via Postgres advisory lock) + +New file `apps/api/src/lib/integrations/linear/refresh-token.ts`: + +```ts +const REFRESH_LOCK_NAMESPACE = 0x4c494e52; // "LINR" — arbitrary, just needs to be stable + +export async function refreshLinearToken(connectionId: string): Promise { + await dbWs.transaction(async (tx) => { + // Single-flight: parallel refreshes will race and both invalidate each other, + // because Linear rotates refresh tokens. Advisory lock serializes per connection. + const lockKey = hashStringToInt(connectionId); + await tx.execute(sql`SELECT pg_advisory_xact_lock(${REFRESH_LOCK_NAMESPACE}, ${lockKey})`); + + const conn = await tx.query.integrationConnections.findFirst({ + where: eq(integrationConnections.id, connectionId), + }); + if (!conn?.refreshToken) { + throw new Error("No refresh token"); + } + + // Re-check expiry under lock — another process may have just refreshed. + if (conn.tokenExpiresAt && conn.tokenExpiresAt > new Date(Date.now() + 60_000)) { + return; // still valid for >60s, someone else refreshed + } + + const response = await fetch("https://api.linear.app/oauth/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: conn.refreshToken, + client_id: env.LINEAR_CLIENT_ID, + client_secret: env.LINEAR_CLIENT_SECRET, + }), + }); + + if (!response.ok) { + const body = await response.json().catch(() => ({})); + if (body?.error === "invalid_grant") { + // Refresh token expired (inactivity) or user revoked the app. + await tx.update(integrationConnections).set({ + disconnectedAt: new Date(), + disconnectReason: "invalid_grant", + }).where(eq(integrationConnections.id, connectionId)); + } + throw new Error(`Linear token refresh failed: ${response.status}`); + } + + const data = await response.json(); + await tx.update(integrationConnections).set({ + accessToken: data.access_token, + refreshToken: data.refresh_token, // rotated; old one is now dead + tokenExpiresAt: new Date(Date.now() + data.expires_in * 1000), + }).where(eq(integrationConnections.id, connectionId)); + }); +} +``` + +#### `getLinearClient` refreshes proactively + +`packages/trpc/src/router/integration/linear/utils.ts`: + +```ts +export async function getLinearClient(organizationId: string): Promise { + const connection = await db.query.integrationConnections.findFirst({ + where: and( + eq(integrationConnections.organizationId, organizationId), + eq(integrationConnections.provider, "linear"), + ), + }); + + if (!connection || connection.disconnectedAt) return null; + + // Refresh if expired or expiring within 5 minutes. + const expiresIn = connection.tokenExpiresAt + ? connection.tokenExpiresAt.getTime() - Date.now() + : Infinity; + + if (expiresIn < 5 * 60 * 1000) { + await refreshLinearToken(connection.id); + // Re-fetch to get the fresh access token written by refreshLinearToken. + const refreshed = await db.query.integrationConnections.findFirst({ + where: eq(integrationConnections.id, connection.id), + }); + if (!refreshed || refreshed.disconnectedAt) return null; + return new LinearClient({ accessToken: refreshed.accessToken }); + } + + return new LinearClient({ accessToken: connection.accessToken }); +} +``` + +#### 401 fallback in API call sites + +The Linear SDK throws errors with status info. Wrap call sites that hit Linear (sync-task route, initial-sync, getTeams in tRPC) so a 401 attempts one refresh-then-retry before propagating: + +```ts +async function callLinear(orgId: string, fn: (client: LinearClient) => Promise): Promise { + let client = await getLinearClient(orgId); + if (!client) throw new Error("Linear not connected"); + + try { + return await fn(client); + } catch (e) { + if (isLinearAuthError(e)) { + const conn = await db.query.integrationConnections.findFirst({/* … */}); + if (conn) await refreshLinearToken(conn.id); + client = await getLinearClient(orgId); + if (!client) throw new Error("Linear connection broken"); + return await fn(client); + } + throw e; + } +} +``` + +#### One-time migration of legacy long-lived tokens + +Linear provides a [migration endpoint](https://linear.app/developers/oauth-2-0-authentication) to upgrade pre-rotation long-lived tokens to the new (access + refresh) pair. Backfill script in `packages/scripts/`: + +```ts +// For each connection where refreshToken IS NULL: +// POST to Linear's migration endpoint with the existing long-lived access_token +// Receive { access_token, refresh_token, expires_in } +// Atomically update the connection +// On error: mark disconnected (token may already be dead) +``` + +Run once at deploy time. Logs each connection's outcome. + +#### UI: surface broken connections + +Integrations page (`apps/web/...integrations/linear/page.tsx`): if `disconnectedAt IS NOT NULL`, replace the "Connected" state with a "Reconnect Linear" CTA that re-runs the OAuth flow. Show `disconnectReason` as supporting copy. + +### Why ship this first + +Token expiry is actively breaking users right now. The team-entity migration is more invasive but less urgent. Ordering: + +1. **Workstream 2 in its own PR**, fast turnaround. Schema changes are additive (`disconnectedAt`, `disconnectReason`, populate `refreshToken`). Backfill script runs at deploy. +2. **Workstream 1 + 3 together** in a follow-up PR. + +--- + +## Workstream 3: `actor=app` switch + connect/error UX + +### `actor=app` + +`apps/api/.../linear/connect/route.ts:50` — change the OAuth scope params to include `actor=app`. Issues created/updated by Superset will then appear as authored by the Superset OAuth app instead of by whoever connected. Standard for listed integrations (Slack, GitHub, Devin all do this). + +```ts +linearAuthUrl.searchParams.set("scope", "read,write,issues:create"); +linearAuthUrl.searchParams.set("actor", "app"); // NEW +``` + +One-line change. No data migration. Existing tokens keep working with their old actor; only newly authored issues after re-auth show "Superset" as author. Worth re-auth-ing once after rollout for consistency, but not required. + +### Integrations UI revamp + +`apps/web/src/app/(dashboard-legacy)/integrations/linear/`: + +Today's UI: +- Connect button → OAuth +- `TeamSelector` dropdown → "Where to create new tasks" → writes `linearConfig.newTasksTeamId` +- `ConnectionControls` → disconnect button +- `ErrorHandler` → reads `?error=` query param + +After: +- Connect button → OAuth (with `actor=app`) +- **"Link Linear team to Superset" picker** → writes `teams.external_provider/id/key` for the org's default team (replaces the `newTasksTeamId` mutation entirely) +- **Connection status panel** → shows `disconnectedAt`/`disconnectReason` from workstream 2, with "Reconnect" CTA when broken +- **Connect-flow consent copy** → "Issues from the Linear team you link will be visible to all members of your Superset organization" (documents the visibility-broadening risk for private Linear teams without engineering around it) +- **Orphaned-issues notice** → if there are tasks with `external_provider='linear'` but no longer matching the linked team's `externalId`, show "X issues from previously-linked teams are no longer syncing — keep or delete?" (UI for actually cleaning up is a follow-up) + +`linearConfig.newTasksTeamId` is dropped from the `LinearConfig` type. The `updateConfig` tRPC mutation is removed. Replaced by a `linkTeam` mutation that takes `(superseTeamId, linearTeamId, linearTeamKey, linearTeamName)` and writes the linkage. + +--- + +## Surface area (combined) + +| Area | Files | Notes | +|---|---|---| +| Schema | `packages/db/src/schema/{schema,relations,types}.ts` + 2 migrations + 1 deploy script | new tables, tasks alter, connection-broken fields, drop `LinearConfig.newTasksTeamId` | +| OAuth refresh | `apps/api/src/lib/integrations/linear/refresh-token.ts` (new) + `apps/api/.../linear/callback/route.ts` + `packages/trpc/src/router/integration/linear/utils.ts` + `packages/scripts/migrate-linear-tokens.ts` (new) | core refresh logic + 1-time migration | +| 401 retry wrapper | `apps/api/.../linear/jobs/{sync-task,initial-sync}/*` + `packages/trpc/.../linear/linear.ts` (getTeams) | call-site wrapping | +| Connect route | `apps/api/.../linear/connect/route.ts` | add `actor=app` | +| tRPC tasks | `packages/trpc/src/router/task/{task,schema}.ts` | rewrite createTask, byIdOrKey, drop bySlug | +| tRPC integrations | `packages/trpc/src/router/integration/linear/linear.ts` | replace `updateConfig` with `linkTeam`, disconnect tweak | +| Linear API routes | `apps/api/.../linear/{webhook,jobs/sync-task,jobs/initial-sync}/*` | drop slug writes, switch to team_id+number, filter by linkage | +| MCP tools | `packages/mcp/src/tools/tasks/*` (5 files) + `packages/mcp-v2/src/tools/tasks/*` | input descriptions, slug→identifier | +| SDK | `packages/sdk/src/resources/tasks.ts` | add `identifier`, deprecate `slug` | +| Desktop UI | TasksTable, KanbanCard, TaskDetailHeader, TaskActionMenu, RunInWorkspacePopover, IssueLinkCommand, LinkedTaskChip, ChatInputFooter, $taskId/page.tsx | display + nav | +| Mention parser | `apps/desktop/.../parseUserMentions/parseUserMentions.ts` | rename output field; logic unchanged | +| Web integrations UI | `apps/web/.../integrations/linear/{page.tsx, components/*}` | reskin TeamSelector → LinearTeamLinker, add disconnected state, consent copy | +| local-db | `packages/local-db/src/schema/schema.ts` + sqlite migration | parallel teams/team_keys/team_sequences mirror | +| Agent launch | `packages/shared/src/agent-launch.ts` | `task.slug` → `task.identifier` for prompt filenames + workspace names | +| Tests | delete `task-slug.test.ts`; new tests for sequence allocation, identifier resolution, retired-key redirect, external_key fallback, refresh single-flight, 401 retry | | + +Estimated 1.5k–2k LOC across both PRs. + +--- + +## Phases + +### PR 1 — Workstream 2 (OAuth refresh, urgent) + +1. Schema additions (`refreshToken` populated, `disconnectedAt`, `disconnectReason`). +2. Callback updated to store refresh token + expiry. +3. `refreshLinearToken` helper with advisory-lock single-flight. +4. `getLinearClient` proactive refresh. +5. 401 retry wrapper at call sites. +6. Deploy-time backfill script for legacy long-lived tokens. +7. UI: surface disconnected state with "Reconnect" CTA. + +Independently shippable. No dependency on workstream 1. + +### PR 2 — Workstreams 1 + 3 (teams + numbering + actor=app + UI revamp) + +1. Schema migration (teams, team_keys, team_sequences, tasks alter) + deploy script. +2. Backend writers + readers switch to identifier. tRPC, MCP tools, SDK adds `identifier` as canonical. `slug` still dual-written. +3. Linear sync routes filter by linkage; outbound uses `team.externalId`. +4. `actor=app` switch in connect route. +5. Web integrations UI revamp. +6. Desktop UI + agent-launch switch to identifier. + +### PR 3 — Cleanup (follow-up after one release) + +1. Drop `tasks.slug` column. +2. Drop SDK `slug` deprecation alias. + +--- + +## Open decisions (defaulted, flag if wrong) + +1. **`@task:ENG-42` fallback via `external_key`**: yes, with partial unique index. +2. **PR titles + branches use our identifier (`SUPER-N`)**, not Linear's. Linear users see different identifiers between our app and Linear's UI — `external_key` is the bridge. +3. **`slug` deprecated for one release**, dual-written, then dropped. +4. **Periodic Linear team poll** for opportunistic key sync: deferred. +5. **Team key derivation on org creation**: uppercase + sanitize org slug, fallback to `TASK` if empty. +6. **No multi-team UI for now**: single default team auto-created lazily on first task. +7. **`actor=app`** for new auths; pre-existing tokens keep their old actor until re-auth. +8. **Orphaned-issue cleanup** on link change: notify only, defer the actual delete UI. + +--- + +## Out of scope / follow-ups + +- Multi-team UI (create/rename/archive teams in org settings). +- Per-team Linear linkage at scale (multiple Superset teams each linking to different Linear teams). +- Team key rename UI (with redirect-history notification to users). +- Periodic Linear teams poll for opportunistic detection of Linear-side renames. +- Drop `tasks.slug` column (separate migration after SDK rollout). +- Cleanup UI for orphaned Linear-synced tasks (post-link-change). +- GitHub integration analog (`#123` style identifiers — would use the same `external_key` fallback mechanism). +- Linear integration directory submission (separate workstream — depends on this work landing first).