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.
+
+
+
+ Reconnect Linear
+
+
+
+
+
+ {disconnectMutation.isPending
+ ? "Disconnecting..."
+ : "Disconnect"}
+
+
+
+
+ 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).