diff --git a/apps/dashboard/scripts/create-dev-auth-account.ts b/apps/dashboard/scripts/create-dev-auth-account.ts
new file mode 100644
index 000000000..ed9131e70
--- /dev/null
+++ b/apps/dashboard/scripts/create-dev-auth-account.ts
@@ -0,0 +1,186 @@
+import { db } from "@notra/db/drizzle";
+import {
+ members,
+ organizations,
+ userBackupCodes,
+ users,
+} from "@notra/db/schema";
+import { WorkOS } from "@workos-inc/node";
+import { and, eq } from "drizzle-orm";
+
+const EMAIL = process.env.DEV_AUTH_EMAIL ?? "mfa-demo@demo.notra.dev";
+const PASSWORD = process.env.DEV_AUTH_PASSWORD ?? "MfaDemo-2026!";
+const ORG_SLUG = process.env.DEV_AUTH_ORG_SLUG ?? "mfa-demo";
+const ORG_NAME = "MFA Demo";
+const FIRST_NAME = "MFA";
+const LAST_NAME = "Demo";
+const RESET_MFA = process.argv.includes("--reset-mfa");
+
+const apiKey = process.env.WORKOS_API_KEY;
+const clientId = process.env.WORKOS_CLIENT_ID;
+if (!(apiKey && clientId)) {
+ throw new Error("WORKOS_API_KEY and WORKOS_CLIENT_ID must be set");
+}
+if (!apiKey.startsWith("sk_test")) {
+ throw new Error("Refusing to run against a non-test WorkOS API key");
+}
+
+const workos = new WorkOS(apiKey, { clientId });
+
+async function ensureWorkOSUser() {
+ const existing = await workos.userManagement.listUsers({ email: EMAIL });
+ const found = existing.data[0];
+ if (found) {
+ await workos.userManagement.updateUser({
+ userId: found.id,
+ password: PASSWORD,
+ emailVerified: true,
+ firstName: FIRST_NAME,
+ lastName: LAST_NAME,
+ });
+ console.log(`WorkOS user exists: ${found.id} (password reset)`);
+ return found;
+ }
+ const created = await workos.userManagement.createUser({
+ email: EMAIL,
+ password: PASSWORD,
+ emailVerified: true,
+ firstName: FIRST_NAME,
+ lastName: LAST_NAME,
+ });
+ console.log(`WorkOS user created: ${created.id}`);
+ return created;
+}
+
+async function resetFactors(workosUserId: string) {
+ const factors = await workos.multiFactorAuth.listUserAuthFactors({
+ userId: workosUserId,
+ });
+ const totpFactors = factors.data.filter((factor) => factor.type === "totp");
+ await Promise.all(
+ totpFactors.map(async (factor) => {
+ await workos.multiFactorAuth.deleteFactor(factor.id);
+ console.log(`Removed ${factor.type} factor ${factor.id}`);
+ })
+ );
+ if (totpFactors.length === 0) {
+ console.log("No TOTP factors to remove");
+ }
+ const skipped = factors.data.length - totpFactors.length;
+ if (skipped > 0) {
+ console.warn(
+ `Left ${skipped} non-TOTP factor(s) in place; they still block password sign-in`
+ );
+ }
+}
+
+async function ensureLocalUser(workosUserId: string) {
+ const byWorkosId = await db.query.users.findFirst({
+ where: eq(users.workosUserId, workosUserId),
+ });
+ if (byWorkosId) {
+ return byWorkosId;
+ }
+ const byEmail = await db.query.users.findFirst({
+ where: eq(users.email, EMAIL),
+ });
+ if (byEmail) {
+ const [linked] = await db
+ .update(users)
+ .set({ workosUserId, emailVerified: true })
+ .where(eq(users.id, byEmail.id))
+ .returning();
+ console.log(`Linked local user ${byEmail.id} to WorkOS`);
+ return linked ?? byEmail;
+ }
+ const [created] = await db
+ .insert(users)
+ .values({
+ id: crypto.randomUUID(),
+ name: `${FIRST_NAME} ${LAST_NAME}`,
+ email: EMAIL,
+ emailVerified: true,
+ workosUserId,
+ })
+ .returning();
+ if (!created) {
+ throw new Error("Failed to insert local user");
+ }
+ console.log(`Local user created: ${created.id}`);
+ return created;
+}
+
+async function ensureOrganization(userId: string) {
+ let organization = await db.query.organizations.findFirst({
+ where: eq(organizations.slug, ORG_SLUG),
+ });
+ let createdHere = false;
+ if (!organization) {
+ createdHere = true;
+ const [created] = await db
+ .insert(organizations)
+ .values({
+ id: crypto.randomUUID(),
+ name: ORG_NAME,
+ slug: ORG_SLUG,
+ createdAt: new Date(),
+ onboardingCompleted: true,
+ onboardingDismissed: true,
+ onboardingAgentRan: true,
+ })
+ .returning();
+ organization = created;
+ console.log(`Organization created: ${ORG_SLUG}`);
+ }
+ if (!organization) {
+ throw new Error("Failed to create organization");
+ }
+ const membership = await db.query.members.findFirst({
+ where: and(
+ eq(members.organizationId, organization.id),
+ eq(members.userId, userId)
+ ),
+ });
+ if (!membership && !createdHere) {
+ throw new Error(
+ `Organization "${ORG_SLUG}" already exists and the dev account is not a member. Pick another DEV_AUTH_ORG_SLUG rather than joining someone else's organization.`
+ );
+ }
+ if (!membership) {
+ await db.insert(members).values({
+ id: crypto.randomUUID(),
+ organizationId: organization.id,
+ userId,
+ role: "owner",
+ createdAt: new Date(),
+ });
+ console.log("Owner membership created");
+ }
+ return organization;
+}
+
+const workosUser = await ensureWorkOSUser();
+const localUser = await ensureLocalUser(workosUser.id);
+if (RESET_MFA) {
+ await resetFactors(workosUser.id);
+ await db
+ .delete(userBackupCodes)
+ .where(eq(userBackupCodes.userId, localUser.id));
+ console.log("Cleared backup codes");
+}
+if (workosUser.externalId !== localUser.id) {
+ await workos.userManagement.updateUser({
+ userId: workosUser.id,
+ externalId: localUser.id,
+ });
+}
+const organization = await ensureOrganization(localUser.id);
+
+console.log("\nDev auth account ready");
+console.log(` Email: ${EMAIL}`);
+console.log(` Password: ${PASSWORD}`);
+console.log(` Org: /${organization.slug}`);
+console.log(
+ "\nTest: sign in at /login, open Settings → Security, set up the authenticator, sign out, sign in again."
+);
+process.exit(0);
diff --git a/apps/dashboard/scripts/mfa-smoke.ts b/apps/dashboard/scripts/mfa-smoke.ts
new file mode 100644
index 000000000..23ba0907a
--- /dev/null
+++ b/apps/dashboard/scripts/mfa-smoke.ts
@@ -0,0 +1,84 @@
+import { AuthenticationException, WorkOS } from "@workos-inc/node";
+
+import { generateTotpCode } from "../src/lib/auth/dev-totp";
+
+const EMAIL = process.env.DEV_AUTH_EMAIL ?? "mfa-demo@demo.notra.dev";
+const PASSWORD = process.env.DEV_AUTH_PASSWORD ?? "MfaDemo-2026!";
+
+const apiKey = process.env.WORKOS_API_KEY;
+const clientId = process.env.WORKOS_CLIENT_ID;
+if (!(apiKey && clientId)) {
+ throw new Error("WORKOS_API_KEY and WORKOS_CLIENT_ID must be set");
+}
+if (!apiKey.startsWith("sk_test")) {
+ throw new Error("Refusing to run against a non-test WorkOS API key");
+}
+
+const workos = new WorkOS(apiKey, { clientId });
+
+const user = (await workos.userManagement.listUsers({ email: EMAIL })).data[0];
+if (!user) {
+ throw new Error(
+ `No WorkOS user for ${EMAIL}; run create-dev-auth-account.ts`
+ );
+}
+
+const enrollment = await workos.multiFactorAuth.createUserAuthFactor({
+ userId: user.id,
+ type: "totp",
+ totpIssuer: "Notra",
+ totpUser: EMAIL,
+});
+const factorId = enrollment.authenticationFactor.id;
+const secret = enrollment.authenticationFactor.totp.secret;
+console.log(`1. Enrolled factor ${factorId}`);
+
+try {
+ const verification = await workos.multiFactorAuth.verifyChallenge({
+ authenticationChallengeId: enrollment.authenticationChallenge.id,
+ code: await generateTotpCode(secret),
+ });
+ console.log(`2. Enrollment challenge valid: ${verification.valid}`);
+
+ let pendingToken: string | undefined;
+ try {
+ await workos.userManagement.authenticateWithPassword({
+ clientId,
+ email: EMAIL,
+ password: PASSWORD,
+ });
+ console.log(
+ "3. Password sign-in succeeded WITHOUT an MFA challenge → MFA is not enabled for this WorkOS environment (Dashboard → Authentication → Multi-Factor Auth)."
+ );
+ } catch (error) {
+ if (!(error instanceof AuthenticationException)) {
+ throw error;
+ }
+ console.log(`3. Password sign-in → ${error.code}`);
+ if (error.code !== "mfa_challenge") {
+ throw error;
+ }
+ pendingToken = error.pendingAuthenticationToken;
+ const factors = error.rawData.authentication_factors ?? [];
+ console.log(` factors in error: ${JSON.stringify(factors)}`);
+ }
+
+ if (pendingToken) {
+ const challenge = await workos.multiFactorAuth.challengeFactor({
+ authenticationFactorId: factorId,
+ });
+ const response = await workos.userManagement.authenticateWithTotp({
+ clientId,
+ pendingAuthenticationToken: pendingToken,
+ authenticationChallengeId: challenge.id,
+ code: await generateTotpCode(secret),
+ });
+ console.log(
+ `4. authenticateWithTotp → session for ${response.user.email} via ${response.authenticationMethod}`
+ );
+ }
+} finally {
+ await workos.multiFactorAuth.deleteFactor(factorId);
+ console.log(`5. Cleaned up factor ${factorId}`);
+}
+process.exit(0);
diff --git a/apps/dashboard/src/app/(auth)/login/page.tsx b/apps/dashboard/src/app/(auth)/login/page.tsx
index de917e523..d9358f92f 100644
--- a/apps/dashboard/src/app/(auth)/login/page.tsx
+++ b/apps/dashboard/src/app/(auth)/login/page.tsx
@@ -2,14 +2,15 @@ import { Suspense } from "react";
import { LoginContent } from "@/components/auth/login-content";
import { LoginFormSkeleton } from "@/components/auth/login-form-skeleton";
+import type { LoginPageProps } from "@/types/auth/login-page";
export const instant = true;
-export default function Login() {
+export default function Login({ searchParams }: LoginPageProps) {
return (
}>
-
+
);
diff --git a/apps/dashboard/src/app/auth/social/callback/route.ts b/apps/dashboard/src/app/auth/social/callback/route.ts
index f4ca1d2b5..e814045e7 100644
--- a/apps/dashboard/src/app/auth/social/callback/route.ts
+++ b/apps/dashboard/src/app/auth/social/callback/route.ts
@@ -4,8 +4,11 @@ import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import type { NextRequest } from "next/server";
+import { LOGIN_MFA_QUERY_KEY, MFA_ERROR_CODES } from "@/constants/security";
import { SOCIAL_AUTH_STATE_COOKIE } from "@/constants/social-auth";
import { UserSyncError, WorkOSAuthError } from "@/lib/auth/errors";
+import { resolveMfaFlow } from "@/lib/auth/mfa";
+import { storePendingMfaFlow } from "@/lib/auth/mfa-cookies";
import { authenticateResolvingOrgSelection } from "@/lib/auth/org-selection";
import { sanitizeReturnTo } from "@/lib/auth/return-to";
import { syncAuthenticatedUser } from "@/lib/auth/sync";
@@ -14,8 +17,15 @@ import { readWorkOSError } from "@/lib/auth/workos-error";
const VERIFICATION_REQUIRED_CODE = "email_verification_required";
interface SocialCallbackOutcome {
- kind: "success" | "failed" | "verification-required";
+ kind:
+ | "success"
+ | "failed"
+ | "verification-required"
+ | "mfa-required"
+ | "mfa-enrollment-required";
pendingAuthenticationToken?: string;
+ authenticationChallengeId?: string;
+ workosUserId?: string;
email?: string;
}
@@ -43,34 +53,74 @@ const exchangeSocialCode = Effect.fn("auth.social.exchangeCode")(function* (
});
});
+const logFailure = (message: string) =>
+ Effect.logWarning("Social sign-in failed").pipe(
+ Effect.annotateLogs({ error: message }),
+ Effect.as({ kind: "failed" })
+ );
+
const mapFailure = (error: WorkOSAuthError | UserSyncError) => {
- if (error instanceof WorkOSAuthError) {
- const info = readWorkOSError(error.error);
-
- if (
- info.code === VERIFICATION_REQUIRED_CODE &&
- info.pendingAuthenticationToken
- ) {
- const outcome: SocialCallbackOutcome = {
- kind: "verification-required",
- pendingAuthenticationToken: info.pendingAuthenticationToken,
- email: info.email ?? undefined,
- };
- return Effect.succeed(outcome);
- }
+ if (!(error instanceof WorkOSAuthError)) {
+ return logFailure(error.message);
}
- return Effect.logWarning("Social sign-in failed").pipe(
- Effect.annotateLogs({
- error:
- error instanceof WorkOSAuthError
- ? readWorkOSError(error.error).message
- : error.message,
- }),
- Effect.as({ kind: "failed" })
- );
+ const info = readWorkOSError(error.error);
+
+ if (
+ info.code === VERIFICATION_REQUIRED_CODE &&
+ info.pendingAuthenticationToken
+ ) {
+ const outcome: SocialCallbackOutcome = {
+ kind: "verification-required",
+ pendingAuthenticationToken: info.pendingAuthenticationToken,
+ email: info.email ?? undefined,
+ };
+ return Effect.succeed(outcome);
+ }
+
+ if (
+ info.code === MFA_ERROR_CODES.ENROLLMENT &&
+ info.pendingAuthenticationToken &&
+ info.userId
+ ) {
+ return Effect.succeed({
+ kind: "mfa-enrollment-required",
+ pendingAuthenticationToken: info.pendingAuthenticationToken,
+ workosUserId: info.userId,
+ email: info.email ?? undefined,
+ });
+ }
+
+ if (info.code === MFA_ERROR_CODES.CHALLENGE) {
+ return resolveMfaFlow(info, info.email ?? "").pipe(
+ Effect.flatMap((mfaResult) => {
+ if (mfaResult?.status !== "mfa-required") {
+ return logFailure(info.message);
+ }
+ return Effect.succeed({
+ kind: "mfa-required",
+ pendingAuthenticationToken: mfaResult.pendingAuthenticationToken,
+ authenticationChallengeId: mfaResult.authenticationChallengeId,
+ email: mfaResult.email || undefined,
+ });
+ }),
+ Effect.catch((mfaError) =>
+ logFailure(readWorkOSError(mfaError.error).message)
+ )
+ );
+ }
+
+ return logFailure(info.message);
};
+function buildMfaLoginUrl(flowId: string, returnTo: string) {
+ const params = new URLSearchParams({
+ [LOGIN_MFA_QUERY_KEY]: flowId,
+ returnTo,
+ });
+ return `/login?${params.toString()}`;
+}
+
export async function GET(request: NextRequest) {
const code = request.nextUrl.searchParams.get("code");
const state = request.nextUrl.searchParams.get("state");
@@ -113,6 +163,26 @@ export async function GET(request: NextRequest) {
redirect(`/login?${params.toString()}`);
}
+ if (outcome.kind === "mfa-required") {
+ const flowId = await storePendingMfaFlow({
+ kind: "challenge",
+ pendingAuthenticationToken: outcome.pendingAuthenticationToken ?? "",
+ authenticationChallengeId: outcome.authenticationChallengeId ?? "",
+ email: outcome.email ?? "",
+ });
+ redirect(buildMfaLoginUrl(flowId, returnTo));
+ }
+
+ if (outcome.kind === "mfa-enrollment-required") {
+ const flowId = await storePendingMfaFlow({
+ kind: "enrollment",
+ pendingAuthenticationToken: outcome.pendingAuthenticationToken ?? "",
+ workosUserId: outcome.workosUserId ?? "",
+ email: outcome.email ?? "",
+ });
+ redirect(buildMfaLoginUrl(flowId, returnTo));
+ }
+
if (outcome.kind === "failed") {
redirect("/login?error=social-sign-in-failed");
}
diff --git a/apps/dashboard/src/app/design-system/auth-flow/page.tsx b/apps/dashboard/src/app/design-system/auth-flow/page.tsx
new file mode 100644
index 000000000..579dc08ac
--- /dev/null
+++ b/apps/dashboard/src/app/design-system/auth-flow/page.tsx
@@ -0,0 +1,5 @@
+import { AuthFlowPlayground } from "@/components/design-system/auth-flow-playground";
+
+export default function AuthFlowPage() {
+ return ;
+}
diff --git a/apps/dashboard/src/app/design-system/page-client.tsx b/apps/dashboard/src/app/design-system/page-client.tsx
index 1cebb2966..28eed1c65 100644
--- a/apps/dashboard/src/app/design-system/page-client.tsx
+++ b/apps/dashboard/src/app/design-system/page-client.tsx
@@ -368,10 +368,6 @@ function ColorSwatch({ name, label }: { name: string; label: string }) {
);
}
-/*
- * Tailwind must see these class strings literally, so each row spells out its
- * own utilities rather than interpolating the token name.
- */
const MOTION_DURATIONS = [
{
token: "duration-instant",
@@ -1758,6 +1754,8 @@ export default function DesignSystemClientPage() {
+
+
diff --git a/apps/dashboard/src/components/auth/login-content.tsx b/apps/dashboard/src/components/auth/login-content.tsx
index 35f6a8cc9..cf5d6d3a7 100644
--- a/apps/dashboard/src/components/auth/login-content.tsx
+++ b/apps/dashboard/src/components/auth/login-content.tsx
@@ -1,35 +1,75 @@
-"use client";
-
-import { parseAsString, useQueryState } from "nuqs";
+import type { PendingAuthStep } from "@notra/schemas/types/dashboard/auth";
import { LoginErrorTracker } from "@/components/auth/login-error-tracker";
import { LoginForm } from "@/components/auth/login-form";
+import { SocialEnrollmentResume } from "@/components/auth/social-enrollment-resume";
import { LOGIN_ERROR_MESSAGES } from "@/constants/login-error-messages";
+import { LOGIN_MFA_QUERY_KEY } from "@/constants/security";
+import { readPendingMfaFlow } from "@/lib/auth/mfa-cookies";
+import type { LoginPageProps, LoginPageStart } from "@/types/auth/login-page";
+
+async function resolveStart(
+ mfa: string | undefined,
+ verify: string | undefined,
+ email: string | undefined
+): Promise {
+ if (mfa) {
+ const flow = await readPendingMfaFlow(mfa);
+ if (flow?.kind === "challenge") {
+ const { kind: _kind, ...challenge } = flow;
+ return { pending: { status: "mfa-required", ...challenge } };
+ }
+ if (flow?.kind === "enrollment") {
+ return { resumeEnrollmentFlowId: mfa };
+ }
+ }
+ if (verify) {
+ const pending: PendingAuthStep = {
+ status: "verification-required",
+ pendingAuthenticationToken: verify,
+ email: email ?? "",
+ };
+ return { pending };
+ }
+ return {};
+}
+
+export async function LoginContent({ searchParams }: LoginPageProps) {
+ const resolvedSearchParams = await searchParams;
+
+ const readParam = (key: string) => {
+ const value = resolvedSearchParams[key];
+ return typeof value === "string" ? value : undefined;
+ };
-export function LoginContent() {
- const [returnTo] = useQueryState("returnTo", parseAsString);
- const [verify] = useQueryState("verify", parseAsString);
- const [email] = useQueryState("email", parseAsString);
- const [errorKey] = useQueryState("error", parseAsString);
+ const returnTo = readParam("returnTo");
+ const verify = readParam("verify");
+ const email = readParam("email");
+ const errorKey = readParam("error");
+ const mfa = readParam(LOGIN_MFA_QUERY_KEY);
const knownErrorKey =
errorKey && Object.hasOwn(LOGIN_ERROR_MESSAGES, errorKey)
? errorKey
: undefined;
+ const start = await resolveStart(mfa, verify, email);
return (
<>
{knownErrorKey ? : null}
-
+ {start.resumeEnrollmentFlowId ? (
+
+ ) : (
+
+ )}
>
);
}
diff --git a/apps/dashboard/src/components/auth/login-form.tsx b/apps/dashboard/src/components/auth/login-form.tsx
index f93904696..2f49741a1 100644
--- a/apps/dashboard/src/components/auth/login-form.tsx
+++ b/apps/dashboard/src/components/auth/login-form.tsx
@@ -2,14 +2,20 @@
import { POSTHOG_EVENTS } from "@notra/posthog/events";
import { loginSchema } from "@notra/schemas/dashboard/auth/credentials";
-import { LoginForm as SharedLoginForm } from "@notra/ui/components/shared/auth/login-form";
import type {
+ RedeemBackupCodeInput,
SignInWithPasswordInput,
VerifyEmailCodeInput,
-} from "@notra/ui/lib/auth-types";
+ VerifyMfaCodeInput,
+} from "@notra/schemas/types/dashboard/auth";
+import { LoginForm as SharedLoginForm } from "@notra/ui/components/shared/auth/login-form";
import { LOGIN_ERROR_CODES } from "@/constants/analytics-events";
import { trackEvent } from "@/lib/analytics/posthog-client";
+import {
+ redeemBackupCodeAction,
+ verifyMfaCodeAction,
+} from "@/lib/auth/mfa-actions";
import {
signInWithPasswordAction,
verifyEmailCodeAction,
@@ -45,6 +51,26 @@ async function verifyEmailCodeTracked(input: VerifyEmailCodeInput) {
return result;
}
+async function verifyMfaCodeTracked(input: VerifyMfaCodeInput) {
+ const result = await verifyMfaCodeAction(input);
+ if (result.status === "error") {
+ trackEvent(POSTHOG_EVENTS.LOGIN_FAILED, {
+ error_code: LOGIN_ERROR_CODES.MFA_REJECTED,
+ });
+ }
+ return result;
+}
+
+async function redeemBackupCodeTracked(input: RedeemBackupCodeInput) {
+ const result = await redeemBackupCodeAction(input);
+ if (result.status === "error") {
+ trackEvent(POSTHOG_EVENTS.LOGIN_FAILED, {
+ error_code: LOGIN_ERROR_CODES.BACKUP_CODE_REJECTED,
+ });
+ }
+ return result;
+}
+
export function LoginForm({ returnTo, ...props }: LoginFormProps) {
return (
);
}
diff --git a/apps/dashboard/src/components/auth/signup-form.tsx b/apps/dashboard/src/components/auth/signup-form.tsx
index 88ca8a912..39f7e366a 100644
--- a/apps/dashboard/src/components/auth/signup-form.tsx
+++ b/apps/dashboard/src/components/auth/signup-form.tsx
@@ -7,16 +7,13 @@ import { AuthFormError } from "@notra/ui/components/shared/auth/auth-form-error"
import { AuthFormHeader } from "@notra/ui/components/shared/auth/auth-form-header";
import { AuthOrDivider } from "@notra/ui/components/shared/auth/auth-or-divider";
import { AuthPasswordField } from "@notra/ui/components/shared/auth/auth-password-field";
+import { AuthPendingStep } from "@notra/ui/components/shared/auth/auth-pending-step";
import { AuthSocialButtons } from "@notra/ui/components/shared/auth/auth-social-buttons";
-import { EmailVerificationForm } from "@notra/ui/components/shared/auth/email-verification-form";
import { CtaButton } from "@notra/ui/components/shared/cta-button";
import { Separator } from "@notra/ui/components/ui/separator";
-import type {
- AuthMethod,
- PendingVerification,
- SocialProvider,
-} from "@notra/ui/lib/auth-types";
+import { useAuthFlow } from "@notra/ui/hooks/use-auth-flow";
import { setLastUsedLoginMethod } from "@notra/ui/lib/last-login-method";
+import type { AuthMethod, SocialProvider } from "@notra/ui/types/auth";
import { useForm } from "@tanstack/react-form";
import { Loader2Icon } from "lucide-react";
import Link from "next/link";
@@ -27,6 +24,10 @@ import { flushSync } from "react-dom";
import { SignupCreditsBanner } from "@/components/auth/signup-credits-banner";
import { SHOW_SIGNUP_CREDITS_BANNER } from "@/constants/signup-credits";
import { trackEvent } from "@/lib/analytics/posthog-client";
+import {
+ redeemBackupCodeAction,
+ verifyMfaCodeAction,
+} from "@/lib/auth/mfa-actions";
import {
signUpWithPasswordAction,
verifyEmailCodeAction,
@@ -62,8 +63,7 @@ export function SignupForm({
}: SignupFormProps) {
const [authMethod, setAuthMethod] = useState(null);
const [formError, setFormError] = useState(null);
- const [pendingVerification, setPendingVerification] =
- useState(null);
+ const flow = useAuthFlow({ onSuccess });
const authInFlightRef = useRef(false);
const [attributionParams] = useQueryStates(marketingAttributionSearchParams, {
history: "replace",
@@ -178,20 +178,10 @@ export function SignupForm({
signupMethod: "email",
});
- if (result.status === "verification-required") {
+ flow.applyResult(result);
+ if (result.status !== "success") {
authInFlightRef.current = false;
setAuthMethod(null);
- setPendingVerification({
- pendingAuthenticationToken: result.pendingAuthenticationToken,
- email: result.email,
- });
- return;
- }
-
- if (onSuccess) {
- onSuccess();
- } else {
- window.location.assign(result.redirectTo);
}
} catch (error) {
console.error("Email signup error:", error);
@@ -202,16 +192,23 @@ export function SignupForm({
},
});
- if (pendingVerification) {
+ if (flow.pending) {
return (
- {
+ flow.reset();
+ setFormError(
+ `Backup code accepted. Two-factor authentication was turned off for ${email}. Sign in to continue.`
+ );
+ }}
+ onResult={flow.applyResult}
+ redeemBackupCode={redeemBackupCodeAction}
returnTo={buildCallbackUrl("email")}
+ step={flow.pending}
verifyEmailCode={verifyEmailCodeAction}
+ verifyMfaCode={verifyMfaCodeAction}
/>
);
}
@@ -246,8 +243,10 @@ export function SignupForm({
name="email"
validators={{
onBlur: ({ value }) =>
- signupSchema.shape.email.safeParse(value).error?.issues[0]
- ?.message,
+ value.length > 0
+ ? signupSchema.shape.email.safeParse(value).error?.issues[0]
+ ?.message
+ : undefined,
onSubmit: ({ value }) =>
signupSchema.shape.email.safeParse(value).error?.issues[0]
?.message,
@@ -270,8 +269,10 @@ export function SignupForm({
name="password"
validators={{
onBlur: ({ value }) =>
- signupSchema.shape.password.safeParse(value).error?.issues[0]
- ?.message,
+ value.length > 0
+ ? signupSchema.shape.password.safeParse(value).error
+ ?.issues[0]?.message
+ : undefined,
onSubmit: ({ value }) =>
signupSchema.shape.password.safeParse(value).error?.issues[0]
?.message,
diff --git a/apps/dashboard/src/components/auth/social-enrollment-resume.tsx b/apps/dashboard/src/components/auth/social-enrollment-resume.tsx
new file mode 100644
index 000000000..ea0e72103
--- /dev/null
+++ b/apps/dashboard/src/components/auth/social-enrollment-resume.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import type { AuthFlowResult } from "@notra/schemas/types/dashboard/auth";
+import { Loader2Icon } from "lucide-react";
+import { useEffect, useRef, useState } from "react";
+
+import { LoginForm } from "@/components/auth/login-form";
+import { resumeSocialEnrollmentAction } from "@/lib/auth/mfa-actions";
+import type { SocialEnrollmentResumeProps } from "@/types/auth/login-page";
+
+const RESUME_ERROR_FALLBACK =
+ "Couldn't continue the two-factor setup. Please sign in again.";
+
+export function SocialEnrollmentResume({
+ flowId,
+ returnTo,
+}: SocialEnrollmentResumeProps) {
+ const [result, setResult] = useState(null);
+ const startedRef = useRef(false);
+
+ useEffect(() => {
+ if (startedRef.current) {
+ return;
+ }
+ startedRef.current = true;
+ resumeSocialEnrollmentAction({ flowId, returnTo })
+ .then(setResult)
+ .catch(() => {
+ setResult({ status: "error", message: RESUME_ERROR_FALLBACK });
+ });
+ }, [flowId, returnTo]);
+
+ if (!result) {
+ return (
+
+
+ Preparing two-factor setup
+
+ );
+ }
+
+ if (result.status === "mfa-enrollment-required") {
+ return ;
+ }
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/components/design-system/auth-flow-playground.tsx b/apps/dashboard/src/components/design-system/auth-flow-playground.tsx
new file mode 100644
index 000000000..990bb30d8
--- /dev/null
+++ b/apps/dashboard/src/components/design-system/auth-flow-playground.tsx
@@ -0,0 +1,143 @@
+"use client";
+
+import { loginSchema } from "@notra/schemas/dashboard/auth/credentials";
+import { LoginForm } from "@notra/ui/components/shared/auth/login-form";
+import { TwoFactorSettings } from "@notra/ui/components/shared/security/two-factor-settings";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@notra/ui/components/ui/card";
+import {
+ Tabs,
+ TabsContent,
+ TabsList,
+ TabsTrigger,
+} from "@notra/ui/components/ui/tabs";
+import { TitleCard } from "@notra/ui/components/ui/title-card";
+
+import {
+ SignedInView,
+ SimulatorPanel,
+} from "@/components/design-system/auth-flow-simulator-panel";
+import { DesignSystemFrame } from "@/components/design-system/design-system-frame";
+import { useAuthFlowPlayground } from "@/components/design-system/use-auth-flow-playground";
+import type { AuthFlowTab } from "@/types/design-system/auth-flow";
+
+const validators = {
+ email: (value: string) =>
+ loginSchema.shape.email.safeParse(value).error?.issues[0]?.message,
+ password: (value: string) =>
+ loginSchema.shape.password.safeParse(value).error?.issues[0]?.message,
+};
+
+export function AuthFlowPlayground() {
+ const flow = useAuthFlowPlayground();
+
+ return (
+
+ End-to-end sign-in and two-factor flow against an in-browser stand-in
+ for WorkOS. TOTP codes are real (RFC 6238). Nothing here talks to
+ WorkOS or the database.
+ >
+ }
+ title="Auth flow playground"
+ >
+
+
flow.setTab(value as AuthFlowTab)}
+ value={flow.tab}
+ >
+
+ Sign in
+
+ Security settings
+
+
+
+
+
+
+ Login
+
+ The real shared login form with simulated server actions.
+
+
+
+ {flow.session ? (
+ flow.setTab("settings")}
+ onSignOut={flow.signOut}
+ session={flow.session}
+ />
+ ) : (
+
+ ({
+ status: "error",
+ message: "Email verification is not simulated here.",
+ })}
+ verifyMfaCode={flow.verifyMfaCode}
+ />
+
+ )}
+
+
+
+
+
+
+
+
+ Add a second step when you sign in with your password.
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/components/design-system/auth-flow-simulator-panel.tsx b/apps/dashboard/src/components/design-system/auth-flow-simulator-panel.tsx
new file mode 100644
index 000000000..88dfba454
--- /dev/null
+++ b/apps/dashboard/src/components/design-system/auth-flow-simulator-panel.tsx
@@ -0,0 +1,210 @@
+"use client";
+
+import { Badge } from "@notra/ui/components/ui/badge";
+import { Label } from "@notra/ui/components/ui/label";
+import { Switch } from "@notra/ui/components/ui/switch";
+import { TitleCard } from "@notra/ui/components/ui/title-card";
+import { useEffect, useState } from "react";
+
+import { Button } from "@/components/button";
+import { TOTP_ISSUER } from "@/constants/security";
+import { generateTotpCode, secondsUntilNextTotp } from "@/lib/auth/dev-totp";
+import type {
+ AuthenticatorWidgetProps,
+ SignedInViewProps,
+ SimulatorPanelProps,
+} from "@/types/design-system/auth-flow";
+
+const DEFAULT_EMAIL = "jane@company.com";
+const TOTP_TICK_MS = 1000;
+
+const CLOCK_TIME_ZONE =
+ typeof Intl === "undefined"
+ ? "UTC"
+ : Intl.DateTimeFormat().resolvedOptions().timeZone;
+const clockFormatter = new Intl.DateTimeFormat("en-GB", {
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ timeZone: CLOCK_TIME_ZONE,
+});
+
+function formatClock(iso: string) {
+ return clockFormatter.format(new Date(iso));
+}
+
+function AuthenticatorWidget({ secret }: AuthenticatorWidgetProps) {
+ const [code, setCode] = useState(null);
+ const [secondsLeft, setSecondsLeft] = useState(() => secondsUntilNextTotp());
+
+ useEffect(() => {
+ if (!secret) {
+ return;
+ }
+ let cancelled = false;
+ const refresh = async () => {
+ const next = await generateTotpCode(secret);
+ if (!cancelled) {
+ setCode(next);
+ setSecondsLeft(secondsUntilNextTotp());
+ }
+ };
+ refresh();
+ const timer = setInterval(refresh, TOTP_TICK_MS);
+ return () => {
+ cancelled = true;
+ clearInterval(timer);
+ };
+ }, [secret]);
+
+ if (!secret) {
+ return (
+
+ No account enrolled yet. Start enrollment to see codes here.
+
+ );
+ }
+
+ return (
+
+
+
+ {TOTP_ISSUER} · {DEFAULT_EMAIL}
+
+
+ {code ?? "······"}
+
+
+
+ {secondsLeft}s
+
+
+ );
+}
+
+export function SimulatorPanel({
+ account,
+ backupCodeCount,
+ orgRequiresMfa,
+ session,
+ pending,
+ settingsEnrollmentSecret,
+ log,
+ onToggleOrgRequiresMfa,
+ onReset,
+}: SimulatorPanelProps) {
+ const authenticatorSecret =
+ account.totpSecret ?? pending?.enrollmentSecret ?? settingsEnrollmentSecret;
+
+ return (
+
+
+ Reset
+
+ }
+ heading="Simulated WorkOS"
+ >
+
+
+
- Email
+ - {account.email}
+
+
+
- Password
+ - {account.password}
+
+
+
- Authenticator app
+ -
+
+ {account.totpSecret ? "Enrolled" : "Off"}
+
+
+
+
+
- Backup codes
+ -
+
+ {backupCodeCount}
+
+
+
+
+
- Session
+ -
+ {session ? (
+
+ password{session.secondFactor ? " + totp" : ""}
+
+ ) : (
+ Signed out
+ )}
+
+
+
+
+
+
+
+ Forces enrollment at sign-in when no factor exists.
+
+
+
+
+
+
+
+
+
+
+
+ {log.length === 0 ? (
+ Nothing yet.
+ ) : (
+
+ {log.map((entry) => (
+ -
+
+ {formatClock(entry.at)}
+
+ {entry.message}
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+export function SignedInView({
+ session,
+ onSignOut,
+ onOpenSettings,
+}: SignedInViewProps) {
+ return (
+
+
Signed in
+
+
{session.email}
+
+ via password
+ {session.secondFactor ? " + authenticator code" : ""} at{" "}
+ {formatClock(session.signedInAt)}
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/components/design-system/design-system-nav.tsx b/apps/dashboard/src/components/design-system/design-system-nav.tsx
index ada09818a..ffd10699f 100644
--- a/apps/dashboard/src/components/design-system/design-system-nav.tsx
+++ b/apps/dashboard/src/components/design-system/design-system-nav.tsx
@@ -4,6 +4,8 @@ import Link from "next/link";
const LINKS = [
{ href: "/design-system#colors", label: "UI kit" },
+ { href: "/design-system#auth-mfa", label: "Auth" },
+ { href: "/design-system/auth-flow", label: "Auth flow" },
{ href: "/design-system#chatgpt-thread", label: "ChatGPT chat" },
{ href: "/design-system#claude-chat-thread", label: "Claude chat" },
{ href: "/design-system#gemini-thread", label: "Gemini chat" },
diff --git a/apps/dashboard/src/components/design-system/use-auth-flow-playground.ts b/apps/dashboard/src/components/design-system/use-auth-flow-playground.ts
new file mode 100644
index 000000000..2f5223262
--- /dev/null
+++ b/apps/dashboard/src/components/design-system/use-auth-flow-playground.ts
@@ -0,0 +1,457 @@
+"use client";
+
+import { BACKUP_CODE_LENGTH } from "@notra/schemas/constants/dashboard/auth";
+import type {
+ AuthFlowResult,
+ RedeemBackupCodeInput,
+ RedeemBackupCodeResult,
+ SignInWithPasswordInput,
+ VerifyMfaCodeInput,
+} from "@notra/schemas/types/dashboard/auth";
+import { normalizeBackupCode } from "@notra/schemas/utils/auth";
+import type {
+ TotpEnrollmentSubmission,
+ TotpVerifyResult,
+} from "@notra/ui/types/auth";
+import type {
+ BackupCodesOutcome,
+ SecurityActionOutcome,
+} from "@notra/ui/types/security";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { toast } from "sonner";
+
+import { TOTP_ISSUER } from "@/constants/security";
+import {
+ buildOtpauthUri,
+ generateTotpSecret,
+ verifyTotpCode,
+} from "@/lib/auth/dev-totp";
+import type {
+ AuthFlowTab,
+ DevAccount,
+ DevLogEntry,
+ DevPendingAuth,
+ DevSession,
+ DevSettingsEnrollment,
+} from "@/types/design-system/auth-flow";
+import { buildPlaceholderQrCode } from "@/utils/design-system-qr";
+
+const DEFAULT_EMAIL = "jane@company.com";
+const DEFAULT_PASSWORD = "playground-pass";
+const SIMULATED_LATENCY_MS = 450;
+const BACKUP_CODE_COUNT = 10;
+const BACKUP_CODE_ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789";
+const MAX_LOG_ENTRIES = 40;
+
+const wait = (ms: number) =>
+ new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+
+function randomId(prefix: string) {
+ return `${prefix}_${crypto.randomUUID().replaceAll("-", "").slice(0, 16)}`;
+}
+
+function randomBackupCodes() {
+ const bytes = new Uint8Array(BACKUP_CODE_COUNT * BACKUP_CODE_LENGTH);
+ crypto.getRandomValues(bytes);
+ return Array.from({ length: BACKUP_CODE_COUNT }, (_, index) =>
+ Array.from(
+ bytes.slice(index * BACKUP_CODE_LENGTH, (index + 1) * BACKUP_CODE_LENGTH),
+ (byte) => BACKUP_CODE_ALPHABET[byte % BACKUP_CODE_ALPHABET.length]
+ ).join("")
+ );
+}
+
+export function useAuthFlowPlayground() {
+ const [account, setAccount] = useState({
+ email: DEFAULT_EMAIL,
+ password: DEFAULT_PASSWORD,
+ totpSecret: null,
+ totpEnrolledAt: null,
+ });
+ const [backupCodes, setBackupCodes] = useState([]);
+ const [orgRequiresMfa, setOrgRequiresMfa] = useState(false);
+ const [session, setSession] = useState(null);
+ const [pending, setPending] = useState(null);
+ const [log, setLog] = useState([]);
+ const [tab, setTab] = useState("sign-in");
+ const [loginKey, setLoginKey] = useState(0);
+
+ const [settingsEnrollment, setSettingsEnrollment] =
+ useState(null);
+ const [isStartingEnrollment, setIsStartingEnrollment] = useState(false);
+ const [removingFactorId, setRemovingFactorId] = useState(null);
+ const pendingSessionRef = useRef(null);
+ const backupCodesRef = useRef([]);
+ const generationRef = useRef(0);
+
+ function updateBackupCodes(
+ next: string[] | ((current: string[]) => string[])
+ ) {
+ const value =
+ typeof next === "function" ? next(backupCodesRef.current) : next;
+ backupCodesRef.current = value;
+ setBackupCodes(value);
+ }
+
+ async function simulateLatency() {
+ const generation = generationRef.current;
+ await wait(SIMULATED_LATENCY_MS);
+ if (generation !== generationRef.current) {
+ throw new Error("Playground was reset");
+ }
+ }
+
+ const accountRef = useRef(account);
+ useEffect(() => {
+ accountRef.current = account;
+ }, [account]);
+
+ const appendLog = useCallback((message: string) => {
+ setLog((current) =>
+ [
+ { id: randomId("log"), at: new Date().toISOString(), message },
+ ...current,
+ ].slice(0, MAX_LOG_ENTRIES)
+ );
+ }, []);
+
+ function reset() {
+ generationRef.current += 1;
+ pendingSessionRef.current = null;
+ setIsStartingEnrollment(false);
+ setRemovingFactorId(null);
+ setAccount({
+ email: DEFAULT_EMAIL,
+ password: DEFAULT_PASSWORD,
+ totpSecret: null,
+ totpEnrolledAt: null,
+ });
+ updateBackupCodes([]);
+ setOrgRequiresMfa(false);
+ setSession(null);
+ setPending(null);
+ setLog([]);
+ setSettingsEnrollment(null);
+ setTab("sign-in");
+ setLoginKey((key) => key + 1);
+ }
+
+ function signOut() {
+ setSession(null);
+ setSettingsEnrollment(null);
+ setTab("sign-in");
+ setLoginKey((key) => key + 1);
+ appendLog("Session ended");
+ }
+
+ async function signInWithPassword(
+ input: SignInWithPasswordInput
+ ): Promise {
+ await simulateLatency();
+ const account = accountRef.current;
+ const matches =
+ input.email.trim().toLowerCase() === account.email &&
+ input.password === account.password;
+ if (!matches) {
+ appendLog("authenticateWithPassword → invalid credentials");
+ return { status: "error", message: "Invalid email or password." };
+ }
+
+ if (account.totpSecret) {
+ const next: DevPendingAuth = {
+ token: randomId("pending"),
+ challengeId: randomId("auth_challenge"),
+ kind: "mfa",
+ enrollmentSecret: null,
+ };
+ setPending(next);
+ appendLog("authenticateWithPassword → mfa_challenge, challenge created");
+ return {
+ status: "mfa-required",
+ pendingAuthenticationToken: next.token,
+ authenticationChallengeId: next.challengeId,
+ email: account.email,
+ };
+ }
+
+ if (orgRequiresMfa) {
+ const secret = generateTotpSecret();
+ const next: DevPendingAuth = {
+ token: randomId("pending"),
+ challengeId: randomId("auth_challenge"),
+ kind: "enrollment",
+ enrollmentSecret: secret,
+ };
+ setPending(next);
+ appendLog("authenticateWithPassword → mfa_enrollment, factor created");
+ return {
+ status: "mfa-enrollment-required",
+ pendingAuthenticationToken: next.token,
+ authenticationChallengeId: next.challengeId,
+ factorId: "auth_factor_playground",
+ email: account.email,
+ qrCode: buildPlaceholderQrCode(1),
+ secret,
+ otpauthUri: buildOtpauthUri(secret, TOTP_ISSUER, account.email),
+ };
+ }
+
+ setSession({
+ email: account.email,
+ secondFactor: null,
+ signedInAt: new Date().toISOString(),
+ });
+ appendLog("authenticateWithPassword → session created");
+ return { status: "success", redirectTo: "#signed-in" };
+ }
+
+ async function verifyMfaCode(
+ input: VerifyMfaCodeInput
+ ): Promise {
+ await simulateLatency();
+ if (
+ !pending ||
+ pending.token !== input.pendingAuthenticationToken ||
+ pending.challengeId !== input.authenticationChallengeId
+ ) {
+ appendLog("authenticateWithTotp → unknown challenge");
+ return {
+ status: "error",
+ message: "This sign-in attempt expired. Please start again.",
+ };
+ }
+
+ const secret =
+ pending.kind === "enrollment"
+ ? pending.enrollmentSecret
+ : account.totpSecret;
+ if (!secret) {
+ return { status: "error", message: "No authenticator enrolled." };
+ }
+
+ const valid = await verifyTotpCode(secret, input.code);
+ if (!valid) {
+ appendLog("authenticateWithTotp → invalid code");
+ return {
+ status: "error",
+ message: "That code didn't work. Please try again.",
+ };
+ }
+
+ const nextSession: DevSession = {
+ email: account.email,
+ secondFactor: "totp",
+ signedInAt: new Date().toISOString(),
+ };
+ setPending(null);
+
+ if (pending.kind !== "enrollment") {
+ appendLog("authenticateWithTotp → code accepted");
+ setSession(nextSession);
+ return { status: "success", redirectTo: "#signed-in" };
+ }
+
+ setAccount((current) => ({
+ ...current,
+ totpSecret: secret,
+ totpEnrolledAt: new Date().toISOString(),
+ }));
+ const issuedCodes = randomBackupCodes();
+ updateBackupCodes(issuedCodes);
+ pendingSessionRef.current = nextSession;
+ appendLog(
+ "authenticateWithTotp → factor verified and enrolled, backup codes issued"
+ );
+ return {
+ status: "enrolled",
+ redirectTo: "#signed-in",
+ backupCodes: issuedCodes,
+ };
+ }
+
+ async function redeemBackupCode(
+ input: RedeemBackupCodeInput
+ ): Promise {
+ await simulateLatency();
+ if (!pending || pending.challengeId !== input.authenticationChallengeId) {
+ return {
+ status: "error",
+ message: "This sign-in attempt expired. Please start again.",
+ };
+ }
+ const normalized = normalizeBackupCode(input.code);
+ if (!backupCodesRef.current.includes(normalized)) {
+ appendLog("redeemBackupCode → rejected");
+ return {
+ status: "error",
+ message: "That backup code isn't valid or was already used.",
+ };
+ }
+ updateBackupCodes([]);
+ setAccount((current) => ({
+ ...current,
+ totpSecret: null,
+ totpEnrolledAt: null,
+ }));
+ setPending(null);
+ appendLog("redeemBackupCode → accepted, authenticator removed");
+ return { status: "recovered", email: account.email };
+ }
+
+ async function startSocialSignIn() {
+ await simulateLatency();
+ appendLog("Social sign-in is not simulated in this playground");
+ throw new Error("Social sign-in is not part of this playground.");
+ }
+
+ async function startSettingsEnrollment() {
+ setIsStartingEnrollment(true);
+ await simulateLatency();
+ const secret = generateTotpSecret();
+ setSettingsEnrollment({
+ secret,
+ qrCode: buildPlaceholderQrCode(2),
+ otpauthUri: buildOtpauthUri(secret, TOTP_ISSUER, account.email),
+ });
+ setIsStartingEnrollment(false);
+ appendLog("createUserAuthFactor → totp factor + challenge created");
+ }
+
+ async function verifySettingsEnrollment({
+ code,
+ }: TotpEnrollmentSubmission): Promise {
+ await simulateLatency();
+ if (!settingsEnrollment) {
+ return { ok: false, message: "Start the setup again." };
+ }
+ const valid = await verifyTotpCode(settingsEnrollment.secret, code);
+ if (!valid) {
+ appendLog("verifyChallenge → invalid code");
+ return { ok: false, message: "That code didn't work. Try again." };
+ }
+ setAccount((current) => ({
+ ...current,
+ totpSecret: settingsEnrollment.secret,
+ totpEnrolledAt: new Date().toISOString(),
+ }));
+ const codes = randomBackupCodes();
+ updateBackupCodes(codes);
+ appendLog("verifyChallenge → factor verified, 2FA on, backup codes issued");
+ toast.success("Two-factor authentication is on");
+ return { ok: true, backupCodes: codes };
+ }
+
+ async function confirmSecondFactor(
+ confirmationCode: string
+ ): Promise {
+ const normalized = normalizeBackupCode(confirmationCode);
+ if (backupCodesRef.current.includes(normalized)) {
+ updateBackupCodes((current) => current.filter((c) => c !== normalized));
+ appendLog("confirmSecondFactor → backup code accepted");
+ return { ok: true };
+ }
+ const secret = accountRef.current.totpSecret;
+ if (secret && (await verifyTotpCode(secret, confirmationCode))) {
+ appendLog("confirmSecondFactor → authenticator code accepted");
+ return { ok: true };
+ }
+ appendLog("confirmSecondFactor → rejected");
+ return {
+ ok: false,
+ message:
+ "That code didn't work. Enter the code from your authenticator app or an unused backup code.",
+ };
+ }
+
+ async function regenerateBackupCodes(
+ confirmationCode: string
+ ): Promise {
+ await simulateLatency();
+ const confirmation = await confirmSecondFactor(confirmationCode);
+ if (!confirmation.ok) {
+ return confirmation;
+ }
+ const codes = randomBackupCodes();
+ updateBackupCodes(codes);
+ appendLog("regenerateBackupCodes → new set issued");
+ return { ok: true, codes };
+ }
+
+ function cancelSettingsEnrollment() {
+ setSettingsEnrollment(null);
+ appendLog("deleteFactor → abandoned enrollment removed");
+ }
+
+ async function removeFactor(
+ factorId: string,
+ confirmationCode: string
+ ): Promise {
+ setRemovingFactorId(factorId);
+ await simulateLatency();
+ const confirmation = await confirmSecondFactor(confirmationCode);
+ if (!confirmation.ok) {
+ setRemovingFactorId(null);
+ return confirmation;
+ }
+ setAccount((current) => ({
+ ...current,
+ totpSecret: null,
+ totpEnrolledAt: null,
+ }));
+ updateBackupCodes([]);
+ setRemovingFactorId(null);
+ appendLog("deleteFactor → 2FA off");
+ toast.success("Two-factor authentication turned off");
+ return { ok: true };
+ }
+
+ const factors = account.totpSecret
+ ? [
+ {
+ id: "auth_factor_playground",
+ issuer: TOTP_ISSUER,
+ createdAt: account.totpEnrolledAt ?? "",
+ },
+ ]
+ : [];
+
+ function completeSignIn() {
+ if (pendingSessionRef.current) {
+ setSession(pendingSessionRef.current);
+ pendingSessionRef.current = null;
+ }
+ setTab("sign-in");
+ }
+
+ return {
+ account,
+ backupCodes,
+ factors,
+ log,
+ loginKey,
+ orgRequiresMfa,
+ pending,
+ session,
+ settingsEnrollment,
+ isStartingEnrollment,
+ removingFactorId,
+ tab,
+ setTab,
+ setOrgRequiresMfa,
+ reset,
+ signOut,
+ completeSignIn,
+ signInWithPassword,
+ verifyMfaCode,
+ redeemBackupCode,
+ startSocialSignIn,
+ startSettingsEnrollment,
+ verifySettingsEnrollment,
+ finishSettingsEnrollment: () => setSettingsEnrollment(null),
+ cancelSettingsEnrollment,
+ regenerateBackupCodes,
+ removeFactor,
+ };
+}
diff --git a/apps/dashboard/src/components/settings/panes/account-pane.tsx b/apps/dashboard/src/components/settings/panes/account-pane.tsx
index 21d242720..13e175ae5 100644
--- a/apps/dashboard/src/components/settings/panes/account-pane.tsx
+++ b/apps/dashboard/src/components/settings/panes/account-pane.tsx
@@ -1,6 +1,7 @@
"use client";
import { Skeleton } from "@notra/ui/components/ui/skeleton";
+import type { SecurityLoadStatus } from "@notra/ui/types/security";
import { useQuery } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
@@ -12,7 +13,22 @@ import { OrganizationsSection } from "@/components/settings/organizations-sectio
import { PrivacySection } from "@/components/settings/privacy-section";
import { ProfileSection } from "@/components/settings/profile-section";
import { SettingsPane } from "@/components/settings/settings-pane";
+import { TwoFactorSection } from "@/components/settings/two-factor-section";
import { authClient } from "@/lib/auth/client";
+import { QUERY_KEYS } from "@/utils/query-keys";
+
+function resolveSecurityStatus(
+ isPending: boolean,
+ isError: boolean
+): SecurityLoadStatus {
+ if (isPending) {
+ return "loading";
+ }
+ if (isError) {
+ return "error";
+ }
+ return "ready";
+}
export function AccountSettingsPane() {
const router = useRouter();
@@ -39,6 +55,18 @@ export function AccountSettingsPane() {
enabled: !!user,
});
+ const securityQuery = useQuery({
+ queryKey: [...QUERY_KEYS.AUTH.security, user?.id],
+ queryFn: async () => {
+ const result = await authClient.security.getOverview();
+ if (result.error) {
+ throw new Error(result.error.message);
+ }
+ return result.data;
+ },
+ enabled: !!user,
+ });
+
if (!user && isSessionPending) {
return (
@@ -72,6 +100,16 @@ export function AccountSettingsPane() {
email={user.email}
hasPasswordAccount={hasPasswordAccount ?? false}
/>
+ securityQuery.refetch()}
+ status={resolveSecurityStatus(
+ securityQuery.isPending,
+ securityQuery.isError
+ )}
+ />
@@ -318,7 +320,7 @@ function SettingsModalBody({
-
+
{isOpen && section ? (
) : null}
diff --git a/apps/dashboard/src/components/settings/two-factor-section.tsx b/apps/dashboard/src/components/settings/two-factor-section.tsx
new file mode 100644
index 000000000..6d1e3fa8f
--- /dev/null
+++ b/apps/dashboard/src/components/settings/two-factor-section.tsx
@@ -0,0 +1,172 @@
+"use client";
+
+import { TwoFactorSettings } from "@notra/ui/components/shared/security/two-factor-settings";
+import { TitleCard } from "@notra/ui/components/ui/title-card";
+import type {
+ TotpEnrollmentSubmission,
+ TotpVerifyResult,
+} from "@notra/ui/types/auth";
+import type {
+ BackupCodesOutcome,
+ SecurityActionOutcome,
+} from "@notra/ui/types/security";
+import { useEffect, useRef, useState } from "react";
+import { toast } from "sonner";
+
+import { authClient } from "@/lib/auth/client";
+import { errorMessageOr } from "@/lib/utils";
+import type {
+ ActiveTotpEnrollment,
+ TwoFactorSectionProps,
+} from "@/types/settings/security";
+
+export function TwoFactorSection({
+ accountLabel,
+ factors,
+ backupCodesRemaining,
+ status,
+ onRefresh,
+}: TwoFactorSectionProps) {
+ const [enrollment, setEnrollment] = useState
(
+ null
+ );
+ const enrollmentRef = useRef(enrollment);
+ useEffect(() => {
+ enrollmentRef.current = enrollment;
+ }, [enrollment]);
+
+ const [isStartingEnrollment, setIsStartingEnrollment] = useState(false);
+ const [removingFactorId, setRemovingFactorId] = useState(null);
+
+ useEffect(() => {
+ return () => {
+ const current = enrollmentRef.current;
+ if (current?.kind === "scanning") {
+ authClient.security
+ .discardTotpEnrollment({ factorId: current.factorId })
+ .catch(() => undefined);
+ }
+ };
+ }, []);
+
+ async function startEnrollment() {
+ setIsStartingEnrollment(true);
+ const result = await authClient.security
+ .startTotpEnrollment()
+ .catch(() => null);
+ setIsStartingEnrollment(false);
+ if (!result || result.error) {
+ toast.error(
+ errorMessageOr(
+ result?.error?.message,
+ "Couldn't start two-factor setup"
+ )
+ );
+ return;
+ }
+ setEnrollment({ kind: "scanning", ...result.data });
+ }
+
+ async function removeFactor(
+ factorId: string,
+ confirmationCode: string
+ ): Promise {
+ setRemovingFactorId(factorId);
+ const result = await authClient.security
+ .removeAuthFactor({ factorId, confirmationCode })
+ .catch(() => null);
+ if (!result) {
+ setRemovingFactorId(null);
+ return { ok: false, message: "Couldn't remove the authenticator app" };
+ }
+ if (result.error) {
+ setRemovingFactorId(null);
+ return { ok: false, message: result.error.message };
+ }
+ toast.success("Two-factor authentication turned off");
+ await onRefresh();
+ setRemovingFactorId(null);
+ return { ok: true };
+ }
+
+ async function verifyEnrollment({
+ code,
+ }: TotpEnrollmentSubmission): Promise {
+ if (!enrollment) {
+ return { ok: false, message: "Start the setup again." };
+ }
+
+ const result = await authClient.security.verifyTotpEnrollment({
+ factorId: enrollment.factorId,
+ authenticationChallengeId: enrollment.authenticationChallengeId,
+ code,
+ });
+
+ if (result.error) {
+ return { ok: false, message: result.error.message };
+ }
+
+ setEnrollment({ ...enrollment, kind: "verified" });
+ if (result.data.warning) {
+ toast.warning(result.data.warning);
+ } else {
+ toast.success("Two-factor authentication is on");
+ }
+ return { ok: true, backupCodes: result.data.backupCodes ?? undefined };
+ }
+
+ async function finishEnrollment() {
+ setEnrollment(null);
+ await onRefresh();
+ }
+
+ function cancelEnrollment() {
+ const current = enrollment;
+ if (current?.kind === "verified") {
+ void finishEnrollment();
+ return;
+ }
+ setEnrollment(null);
+ if (current?.kind === "scanning") {
+ authClient.security
+ .discardTotpEnrollment({ factorId: current.factorId })
+ .catch(() => undefined);
+ }
+ }
+
+ async function regenerateBackupCodes(
+ confirmationCode: string
+ ): Promise {
+ const result = await authClient.security.regenerateBackupCodes({
+ confirmationCode,
+ });
+ if (result.error) {
+ return { ok: false, message: result.error.message };
+ }
+ await onRefresh();
+ return { ok: true, codes: result.data.codes };
+ }
+
+ return (
+
+
+ onRefresh()}
+ onStartEnrollment={startEnrollment}
+ onVerifyEnrollment={verifyEnrollment}
+ removingFactorId={removingFactorId}
+ status={status}
+ />
+
+
+ );
+}
diff --git a/apps/dashboard/src/constants/actions.ts b/apps/dashboard/src/constants/actions.ts
new file mode 100644
index 000000000..70687b762
--- /dev/null
+++ b/apps/dashboard/src/constants/actions.ts
@@ -0,0 +1,3 @@
+export const ACTION_ERROR_CODES = {
+ INVALID_INPUT: "invalid_input",
+} as const;
diff --git a/apps/dashboard/src/constants/analytics-events.ts b/apps/dashboard/src/constants/analytics-events.ts
index 00ee96e08..1766209b8 100644
--- a/apps/dashboard/src/constants/analytics-events.ts
+++ b/apps/dashboard/src/constants/analytics-events.ts
@@ -55,6 +55,8 @@ export const WORKOS_AUTH_METHOD_TO_ANALYTICS: Record<
export const LOGIN_ERROR_CODES = {
PASSWORD_REJECTED: "password_rejected",
VERIFICATION_REJECTED: "verification_rejected",
+ MFA_REJECTED: "mfa_rejected",
+ BACKUP_CODE_REJECTED: "backup_code_rejected",
} as const;
export const PASSWORD_RESET_OUTCOMES = {
diff --git a/apps/dashboard/src/constants/design-system-catalog.ts b/apps/dashboard/src/constants/design-system-catalog.ts
index 8549ae8b4..c64d5569d 100644
--- a/apps/dashboard/src/constants/design-system-catalog.ts
+++ b/apps/dashboard/src/constants/design-system-catalog.ts
@@ -79,6 +79,12 @@ export const DESIGN_SYSTEM_CATALOG: DesignSystemCatalogItem[] = [
href: "/design-system#braille-loader",
level: "section",
},
+ {
+ id: "auth-mfa",
+ label: "Auth · Two-factor",
+ href: "/design-system/auth-flow",
+ level: "section",
+ },
{
id: "claude-session",
label: "Claude · Session",
@@ -309,11 +315,6 @@ export const DESIGN_SYSTEM_CATALOG: DesignSystemCatalogItem[] = [
},
];
-/*
- * The displayed section number is the entry's position in the list, so it is
- * derived here rather than stored on every entry. Inserting a section no longer
- * means renumbering every one that follows it.
- */
export const DESIGN_SYSTEM_CATALOG_BY_ID = Object.fromEntries(
DESIGN_SYSTEM_CATALOG.map((item, index) => [
item.id,
diff --git a/apps/dashboard/src/constants/security.ts b/apps/dashboard/src/constants/security.ts
new file mode 100644
index 000000000..d595f0886
--- /dev/null
+++ b/apps/dashboard/src/constants/security.ts
@@ -0,0 +1,22 @@
+export const TOTP_ISSUER = "Notra";
+export const TOTP_FACTOR_TYPE = "totp";
+
+export const MFA_ATTEMPT_COOKIE = "notra_mfa_attempt";
+export const MFA_PENDING_COOKIE_PREFIX = "notra_mfa_pending";
+export const TOTP_ENROLLMENT_COOKIE = "notra_totp_enrollment";
+export const MFA_COOKIE_MAX_AGE_SECONDS = 10 * 60;
+
+export const MFA_ERROR_CODES = {
+ CHALLENGE: "mfa_challenge",
+ ENROLLMENT: "mfa_enrollment",
+} as const;
+
+export const SECURITY_ERROR_CODES = {
+ INVALID_CODE: "invalid_code",
+ UNAVAILABLE: "unavailable",
+} as const;
+
+export const LOGIN_MFA_QUERY_KEY = "mfa";
+
+export const BACKUP_CODE_COUNT = 10;
+export const BACKUP_CODE_ALPHABET = "abcdefghjkmnpqrstuvwxyz23456789";
diff --git a/apps/dashboard/src/constants/settings.ts b/apps/dashboard/src/constants/settings.ts
index dbc6bba33..884d3bbc0 100644
--- a/apps/dashboard/src/constants/settings.ts
+++ b/apps/dashboard/src/constants/settings.ts
@@ -65,6 +65,12 @@ export const SETTINGS_NAV_GROUPS: readonly SettingsNavGroup[] = [
"github",
"name",
"delete account",
+ "security",
+ "2fa",
+ "mfa",
+ "two-factor",
+ "authenticator",
+ "backup codes",
],
},
{
diff --git a/apps/dashboard/src/lib/actions/errors.ts b/apps/dashboard/src/lib/actions/errors.ts
new file mode 100644
index 000000000..9ae022c5b
--- /dev/null
+++ b/apps/dashboard/src/lib/actions/errors.ts
@@ -0,0 +1,7 @@
+import { Data } from "effect";
+
+export class ActionFailure extends Data.TaggedError("ActionFailure")<{
+ readonly message: string;
+ readonly code?: string;
+ readonly cause?: unknown;
+}> {}
diff --git a/apps/dashboard/src/lib/organizations/run-action.ts b/apps/dashboard/src/lib/actions/run-action.ts
similarity index 57%
rename from apps/dashboard/src/lib/organizations/run-action.ts
rename to apps/dashboard/src/lib/actions/run-action.ts
index d11800cab..a081b7507 100644
--- a/apps/dashboard/src/lib/organizations/run-action.ts
+++ b/apps/dashboard/src/lib/actions/run-action.ts
@@ -1,26 +1,23 @@
import { Effect } from "effect";
-import { OrganizationActionError } from "@/lib/organizations/errors";
+import { ActionFailure } from "@/lib/actions/errors";
import type { ActionResult } from "@/types/organizations/actions";
-export function runOrganizationAction(
- effect: Effect.Effect
+export function runAction(
+ effect: Effect.Effect
): Promise> {
return Effect.runPromise(
effect.pipe(
Effect.catchDefect((defect) =>
Effect.fail(
- new OrganizationActionError({
- message: "Something went wrong",
- cause: defect,
- })
+ new ActionFailure({ message: "Something went wrong", cause: defect })
)
),
Effect.match({
onSuccess: (data): ActionResult => ({ data, error: null }),
onFailure: (error): ActionResult => ({
data: null,
- error: { message: error.message },
+ error: { message: error.message, code: error.code },
}),
})
)
diff --git a/apps/dashboard/src/lib/organizations/validate-input.ts b/apps/dashboard/src/lib/actions/validate-input.ts
similarity index 66%
rename from apps/dashboard/src/lib/organizations/validate-input.ts
rename to apps/dashboard/src/lib/actions/validate-input.ts
index 441cd1706..97cb0fa3b 100644
--- a/apps/dashboard/src/lib/organizations/validate-input.ts
+++ b/apps/dashboard/src/lib/actions/validate-input.ts
@@ -1,19 +1,21 @@
import { Effect } from "effect";
import type * as z from "zod";
-import { OrganizationActionError } from "@/lib/organizations/errors";
+import { ACTION_ERROR_CODES } from "@/constants/actions";
+import { ActionFailure } from "@/lib/actions/errors";
const DEFAULT_INVALID_INPUT_MESSAGE = "Invalid input";
export function validateActionInput(
schema: Schema,
input: unknown
-): Effect.Effect, OrganizationActionError> {
+): Effect.Effect, ActionFailure> {
const result = schema.safeParse(input);
if (!result.success) {
return Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
+ code: ACTION_ERROR_CODES.INVALID_INPUT,
message:
result.error.issues[0]?.message ?? DEFAULT_INVALID_INPUT_MESSAGE,
})
diff --git a/apps/dashboard/src/lib/auth/auth-flow.ts b/apps/dashboard/src/lib/auth/auth-flow.ts
new file mode 100644
index 000000000..e52da7789
--- /dev/null
+++ b/apps/dashboard/src/lib/auth/auth-flow.ts
@@ -0,0 +1,157 @@
+import { POSTHOG_EVENTS, type PostHogEventName } from "@notra/posthog/events";
+import type { PostHogProperties } from "@notra/posthog/types/posthog";
+import type { AuthFlowResult } from "@notra/schemas/types/dashboard/auth";
+import { saveSession } from "@workos-inc/authkit-nextjs";
+import type { AuthenticationResponse } from "@workos-inc/node";
+import { Effect } from "effect";
+
+import { ANALYTICS_AUTH_METHODS } from "@/constants/analytics-events";
+import { toAnalyticsAuthMethod } from "@/lib/analytics/auth-method";
+import { trackServerEvent } from "@/lib/analytics/posthog-server";
+import { readRequestHeaders } from "@/lib/analytics/request-headers";
+import { UserSyncError, WorkOSAuthError } from "@/lib/auth/errors";
+import { resolveMfaFlow } from "@/lib/auth/mfa";
+import { sanitizeReturnTo } from "@/lib/auth/return-to";
+import { syncAuthenticatedUser } from "@/lib/auth/sync";
+import { readWorkOSError } from "@/lib/auth/workos-error";
+
+const VERIFICATION_REQUIRED_CODE = "email_verification_required";
+const DEFAULT_POST_LOGIN_PATH = "/callback";
+
+export async function trackAuthEvent(
+ event: PostHogEventName,
+ properties?: PostHogProperties,
+ userId?: string | null
+) {
+ const requestHeaders = await readRequestHeaders();
+ trackServerEvent({ event, headers: requestHeaders, userId, properties });
+}
+
+export function getWorkOSClientId() {
+ const clientId = process.env.WORKOS_CLIENT_ID;
+ if (!clientId) {
+ throw new Error("WORKOS_CLIENT_ID must be defined");
+ }
+ return clientId;
+}
+
+export const tryWorkOSAuth = (run: () => Promise) =>
+ Effect.tryPromise({
+ try: run,
+ catch: (error) => new WorkOSAuthError({ error }),
+ });
+
+export const completeAuthentication = Effect.fn("auth.completeSession")(
+ function* (
+ response: AuthenticationResponse,
+ returnTo?: string | null,
+ completionEvent?: PostHogEventName
+ ) {
+ yield* Effect.tryPromise({
+ try: () =>
+ saveSession(
+ {
+ accessToken: response.accessToken,
+ refreshToken: response.refreshToken,
+ user: response.user,
+ impersonator: response.impersonator,
+ authenticationMethod: response.authenticationMethod,
+ },
+ process.env.APP_URL ?? "http://localhost:3000"
+ ),
+ catch: (cause) =>
+ new UserSyncError({ message: "Failed to persist session", cause }),
+ });
+
+ const localUser = yield* syncAuthenticatedUser({
+ workosUser: response.user,
+ oauthTokens: response.oauthTokens,
+ authenticationMethod: response.authenticationMethod,
+ });
+
+ if (completionEvent) {
+ yield* Effect.promise(() =>
+ trackAuthEvent(
+ completionEvent,
+ { method: toAnalyticsAuthMethod(response.authenticationMethod) },
+ localUser.id
+ )
+ );
+ }
+
+ return {
+ redirectTo: sanitizeReturnTo(returnTo ?? null) ?? DEFAULT_POST_LOGIN_PATH,
+ localUserId: localUser.id,
+ };
+ }
+);
+
+export const signedIn = ({
+ redirectTo,
+}: {
+ redirectTo: string;
+}): AuthFlowResult => ({ status: "success", redirectTo });
+
+const mapAuthFailure =
+ (email: string) =>
+ (error: WorkOSAuthError | UserSyncError | { message: string }) => {
+ if (!(error instanceof WorkOSAuthError)) {
+ return Effect.succeed({
+ status: "error",
+ message: error.message,
+ });
+ }
+
+ const info = readWorkOSError(error.error);
+
+ if (
+ info.code === VERIFICATION_REQUIRED_CODE &&
+ info.pendingAuthenticationToken
+ ) {
+ const pendingToken = info.pendingAuthenticationToken;
+ return Effect.promise(() =>
+ trackAuthEvent(POSTHOG_EVENTS.EMAIL_VERIFICATION_REQUIRED, {
+ method: ANALYTICS_AUTH_METHODS.PASSWORD,
+ })
+ ).pipe(
+ Effect.as({
+ status: "verification-required",
+ pendingAuthenticationToken: pendingToken,
+ email,
+ })
+ );
+ }
+
+ return resolveMfaFlow(info, email).pipe(
+ Effect.flatMap((mfaResult) => {
+ if (!mfaResult) {
+ return Effect.succeed({
+ status: "error",
+ message: info.message,
+ });
+ }
+
+ const event =
+ mfaResult.status === "mfa-required"
+ ? POSTHOG_EVENTS.MFA_CHALLENGE_REQUIRED
+ : POSTHOG_EVENTS.MFA_ENROLLMENT_REQUIRED;
+
+ return Effect.promise(() =>
+ trackAuthEvent(event, { method: ANALYTICS_AUTH_METHODS.PASSWORD })
+ ).pipe(Effect.as(mfaResult));
+ }),
+ Effect.catch((mfaError) =>
+ Effect.succeed({
+ status: "error",
+ message: readWorkOSError(mfaError.error).message,
+ })
+ )
+ );
+ };
+
+export function runAuthFlow(
+ email: string,
+ flow: Effect.Effect
+): Promise {
+ return Effect.runPromise(flow.pipe(Effect.catch(mapAuthFailure(email))));
+}
diff --git a/apps/dashboard/src/lib/auth/backup-codes.ts b/apps/dashboard/src/lib/auth/backup-codes.ts
new file mode 100644
index 000000000..30694e2f3
--- /dev/null
+++ b/apps/dashboard/src/lib/auth/backup-codes.ts
@@ -0,0 +1,106 @@
+import { createHash, randomInt } from "node:crypto";
+
+import { db } from "@notra/db/drizzle";
+import { userBackupCodes } from "@notra/db/schema";
+import { BACKUP_CODE_LENGTH } from "@notra/schemas/constants/dashboard/auth";
+import { normalizeBackupCode } from "@notra/schemas/utils/auth";
+import { and, eq, isNull, sql } from "drizzle-orm";
+
+import { BACKUP_CODE_ALPHABET, BACKUP_CODE_COUNT } from "@/constants/security";
+
+function hashBackupCode(code: string): string {
+ return createHash("sha256").update(normalizeBackupCode(code)).digest("hex");
+}
+
+function generateBackupCode(): string {
+ let code = "";
+ for (let index = 0; index < BACKUP_CODE_LENGTH; index += 1) {
+ code += BACKUP_CODE_ALPHABET[randomInt(BACKUP_CODE_ALPHABET.length)];
+ }
+ return code;
+}
+
+const lockBackupCodes = (
+ tx: Parameters[0]>[0],
+ userId: string
+) => tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${userId}))`);
+
+export async function replaceBackupCodes(userId: string): Promise {
+ const codes = Array.from({ length: BACKUP_CODE_COUNT }, generateBackupCode);
+ await db.transaction(async (tx) => {
+ await lockBackupCodes(tx, userId);
+ await tx.delete(userBackupCodes).where(eq(userBackupCodes.userId, userId));
+ await tx.insert(userBackupCodes).values(
+ codes.map((code) => ({
+ id: crypto.randomUUID(),
+ userId,
+ codeHash: hashBackupCode(code),
+ }))
+ );
+ });
+ return codes;
+}
+
+export async function clearBackupCodes(userId: string): Promise {
+ await db.transaction(async (tx) => {
+ await lockBackupCodes(tx, userId);
+ await tx.delete(userBackupCodes).where(eq(userBackupCodes.userId, userId));
+ });
+}
+
+export async function countRemainingBackupCodes(
+ userId: string
+): Promise {
+ const rows = await db
+ .select({ id: userBackupCodes.id })
+ .from(userBackupCodes)
+ .where(
+ and(eq(userBackupCodes.userId, userId), isNull(userBackupCodes.usedAt))
+ );
+ return rows.length;
+}
+
+export async function hasBackupCodes(userId: string): Promise {
+ const [row] = await db
+ .select({ id: userBackupCodes.id })
+ .from(userBackupCodes)
+ .where(eq(userBackupCodes.userId, userId))
+ .limit(1);
+ return Boolean(row);
+}
+
+export async function hasUnusedBackupCode(
+ userId: string,
+ code: string
+): Promise {
+ const [row] = await db
+ .select({ id: userBackupCodes.id })
+ .from(userBackupCodes)
+ .where(
+ and(
+ eq(userBackupCodes.userId, userId),
+ eq(userBackupCodes.codeHash, hashBackupCode(code)),
+ isNull(userBackupCodes.usedAt)
+ )
+ )
+ .limit(1);
+ return Boolean(row);
+}
+
+export async function consumeBackupCode(
+ userId: string,
+ code: string
+): Promise {
+ const rows = await db
+ .update(userBackupCodes)
+ .set({ usedAt: new Date() })
+ .where(
+ and(
+ eq(userBackupCodes.userId, userId),
+ eq(userBackupCodes.codeHash, hashBackupCode(code)),
+ isNull(userBackupCodes.usedAt)
+ )
+ )
+ .returning({ id: userBackupCodes.id });
+ return rows.length > 0;
+}
diff --git a/apps/dashboard/src/lib/auth/client.ts b/apps/dashboard/src/lib/auth/client.ts
index b2ec56943..12e653b38 100644
--- a/apps/dashboard/src/lib/auth/client.ts
+++ b/apps/dashboard/src/lib/auth/client.ts
@@ -4,6 +4,14 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
import { resetPostHogIdentity } from "@/lib/analytics/posthog-client";
import { isNextRedirectError } from "@/lib/auth/redirect-error";
+import {
+ discardTotpEnrollmentAction,
+ getSecurityOverviewAction,
+ regenerateBackupCodesAction,
+ removeAuthFactorAction,
+ startTotpEnrollmentAction,
+ verifyTotpEnrollmentAction,
+} from "@/lib/auth/security-actions";
import {
deleteUserAction,
listAccountsAction,
@@ -110,6 +118,14 @@ export const authClient = {
requestPasswordReset: requestPasswordResetAction,
listAccounts: listAccountsAction,
unlinkAccount: unlinkAccountAction,
+ security: {
+ getOverview: getSecurityOverviewAction,
+ startTotpEnrollment: startTotpEnrollmentAction,
+ verifyTotpEnrollment: verifyTotpEnrollmentAction,
+ discardTotpEnrollment: discardTotpEnrollmentAction,
+ removeAuthFactor: removeAuthFactorAction,
+ regenerateBackupCodes: regenerateBackupCodesAction,
+ },
organization: {
create: createOrganizationAction,
update: updateOrganizationAction,
diff --git a/apps/dashboard/src/lib/auth/dev-totp.ts b/apps/dashboard/src/lib/auth/dev-totp.ts
new file mode 100644
index 000000000..f52b84b98
--- /dev/null
+++ b/apps/dashboard/src/lib/auth/dev-totp.ts
@@ -0,0 +1,126 @@
+const BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+const BASE32_BITS_PER_CHAR = 5;
+const BYTE_BITS = 8;
+const BYTE_MASK = 0xff;
+const SECRET_BYTES = 20;
+const COUNTER_BYTES = 8;
+const TRUNCATION_MASK = 0x0f;
+const HOTP_MODULUS = 10 ** 6;
+const TOTP_STEP_SECONDS = 30;
+const MS_PER_SECOND = 1000;
+const NON_BASE32_REGEX = /[^A-Z2-7]/g;
+
+function base32Encode(bytes: Uint8Array): string {
+ let bits = 0;
+ let value = 0;
+ let output = "";
+ for (const byte of bytes) {
+ value = (value << BYTE_BITS) | byte;
+ bits += BYTE_BITS;
+ while (bits >= BASE32_BITS_PER_CHAR) {
+ output +=
+ BASE32_ALPHABET[(value >>> (bits - BASE32_BITS_PER_CHAR)) & 0x1f];
+ bits -= BASE32_BITS_PER_CHAR;
+ }
+ }
+ if (bits > 0) {
+ output += BASE32_ALPHABET[(value << (BASE32_BITS_PER_CHAR - bits)) & 0x1f];
+ }
+ return output;
+}
+
+function base32Decode(secret: string): Uint8Array {
+ const normalized = secret.toUpperCase().replace(NON_BASE32_REGEX, "");
+ const bytes: number[] = [];
+ let bits = 0;
+ let value = 0;
+ for (const char of normalized) {
+ value = (value << BASE32_BITS_PER_CHAR) | BASE32_ALPHABET.indexOf(char);
+ bits += BASE32_BITS_PER_CHAR;
+ if (bits >= BYTE_BITS) {
+ bytes.push((value >>> (bits - BYTE_BITS)) & BYTE_MASK);
+ bits -= BYTE_BITS;
+ }
+ }
+ const output = new Uint8Array(new ArrayBuffer(bytes.length));
+ output.set(bytes);
+ return output;
+}
+
+export function generateTotpSecret(): string {
+ const bytes = new Uint8Array(SECRET_BYTES);
+ crypto.getRandomValues(bytes);
+ return base32Encode(bytes);
+}
+
+export function buildOtpauthUri(
+ secret: string,
+ issuer: string,
+ account: string
+): string {
+ const label = encodeURIComponent(`${issuer}:${account}`);
+ const params = new URLSearchParams({
+ secret,
+ issuer,
+ algorithm: "SHA1",
+ digits: "6",
+ period: String(TOTP_STEP_SECONDS),
+ });
+ return `otpauth://totp/${label}?${params.toString()}`;
+}
+
+async function hotp(secret: string, counter: number): Promise {
+ const key = await crypto.subtle.importKey(
+ "raw",
+ base32Decode(secret),
+ { name: "HMAC", hash: "SHA-1" },
+ false,
+ ["sign"]
+ );
+ const counterBytes = new Uint8Array(COUNTER_BYTES);
+ let remaining = counter;
+ for (let index = COUNTER_BYTES - 1; index >= 0; index -= 1) {
+ counterBytes[index] = remaining & BYTE_MASK;
+ remaining = Math.floor(remaining / (BYTE_MASK + 1));
+ }
+ const digest = new Uint8Array(
+ await crypto.subtle.sign("HMAC", key, counterBytes)
+ );
+ const offset = (digest.at(-1) ?? 0) & TRUNCATION_MASK;
+ const binary =
+ (((digest[offset] ?? 0) & 0x7f) << 24) |
+ (((digest[offset + 1] ?? 0) & BYTE_MASK) << 16) |
+ (((digest[offset + 2] ?? 0) & BYTE_MASK) << 8) |
+ ((digest[offset + 3] ?? 0) & BYTE_MASK);
+ return String(binary % HOTP_MODULUS).padStart(6, "0");
+}
+
+function totpCounter(timestampMs = Date.now()): number {
+ return Math.floor(timestampMs / MS_PER_SECOND / TOTP_STEP_SECONDS);
+}
+
+export function secondsUntilNextTotp(timestampMs = Date.now()): number {
+ const elapsed = Math.floor(timestampMs / MS_PER_SECOND) % TOTP_STEP_SECONDS;
+ return TOTP_STEP_SECONDS - elapsed;
+}
+
+export function generateTotpCode(
+ secret: string,
+ timestampMs = Date.now()
+): Promise {
+ return hotp(secret, totpCounter(timestampMs));
+}
+
+export async function verifyTotpCode(
+ secret: string,
+ code: string,
+ window = 1
+): Promise {
+ const counter = totpCounter();
+ for (let delta = -window; delta <= window; delta += 1) {
+ if ((await hotp(secret, counter + delta)) === code) {
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/apps/dashboard/src/lib/auth/mfa-actions.ts b/apps/dashboard/src/lib/auth/mfa-actions.ts
new file mode 100644
index 000000000..a7562fa9d
--- /dev/null
+++ b/apps/dashboard/src/lib/auth/mfa-actions.ts
@@ -0,0 +1,299 @@
+"use server";
+
+import { db } from "@notra/db/drizzle";
+import { users } from "@notra/db/schema";
+import { POSTHOG_EVENTS } from "@notra/posthog/events";
+import {
+ redeemBackupCodeInputSchema,
+ resumeSocialEnrollmentInputSchema,
+ verifyMfaCodeInputSchema,
+} from "@notra/schemas/dashboard/auth/mfa";
+import type {
+ AuthFlowResult,
+ RedeemBackupCodeInput,
+ RedeemBackupCodeResult,
+ ResumeSocialEnrollmentInput,
+ VerifyMfaCodeInput,
+} from "@notra/schemas/types/dashboard/auth";
+import { getWorkOS } from "@workos-inc/authkit-nextjs";
+import { eq } from "drizzle-orm";
+import { Effect } from "effect";
+
+import { ANALYTICS_AUTH_METHODS } from "@/constants/analytics-events";
+import { TOTP_FACTOR_TYPE } from "@/constants/security";
+import {
+ completeAuthentication,
+ getWorkOSClientId,
+ runAuthFlow,
+ signedIn,
+ trackAuthEvent,
+ tryWorkOSAuth,
+} from "@/lib/auth/auth-flow";
+import {
+ clearBackupCodes,
+ consumeBackupCode,
+ hasBackupCodes,
+ hasUnusedBackupCode,
+ replaceBackupCodes,
+} from "@/lib/auth/backup-codes";
+import { beginTotpEnrollment } from "@/lib/auth/mfa";
+import {
+ clearAllPendingMfaFlows,
+ clearMfaAttemptCookie,
+ clearPendingMfaFlow,
+ readMfaAttempt,
+ readPendingMfaFlow,
+} from "@/lib/auth/mfa-cookies";
+import { authenticateResolvingOrgSelection } from "@/lib/auth/org-selection";
+import { readWorkOSError } from "@/lib/auth/workos-error";
+import type { MfaAttempt } from "@/types/auth/mfa-cookies";
+import { isAccountRateLimited, ratelimit } from "@/utils/ratelimit";
+
+const RATE_LIMITED_MESSAGE = "Too many attempts. Please try again shortly.";
+const ATTEMPT_EXPIRED_MESSAGE =
+ "This sign-in attempt expired. Please start again.";
+const BACKUP_CODE_REJECTED_MESSAGE =
+ "That backup code isn't valid or was already used.";
+
+async function isMfaVerifyRateLimited(attempt: MfaAttempt) {
+ if (
+ await isAccountRateLimited(
+ ratelimit.mfaVerify,
+ `challenge:${attempt.authenticationChallengeId}`
+ )
+ ) {
+ return true;
+ }
+ return isAccountRateLimited(
+ ratelimit.mfaVerify,
+ `user:${attempt.workosUserId}`
+ );
+}
+
+async function readMatchingAttempt(authenticationChallengeId: string) {
+ const attempt = await readMfaAttempt();
+ return attempt?.authenticationChallengeId === authenticationChallengeId
+ ? attempt
+ : null;
+}
+
+const attemptAfterSignIn = (run: () => Promise, what: string) =>
+ Effect.tryPromise(run).pipe(
+ Effect.catch((error) =>
+ Effect.logWarning(`MFA sign-in succeeded but ${what} failed`).pipe(
+ Effect.annotateLogs({ error: String(error.cause) }),
+ Effect.as(null)
+ )
+ )
+ );
+
+export async function verifyMfaCodeAction(
+ rawInput: VerifyMfaCodeInput
+): Promise {
+ const parsed = verifyMfaCodeInputSchema.safeParse(rawInput);
+
+ if (!parsed.success) {
+ return {
+ status: "error",
+ message: parsed.error.issues[0]?.message ?? "Invalid code",
+ };
+ }
+
+ const attempt = await readMatchingAttempt(
+ parsed.data.authenticationChallengeId
+ );
+ if (!attempt) {
+ return { status: "error", message: ATTEMPT_EXPIRED_MESSAGE };
+ }
+ if (await isMfaVerifyRateLimited(attempt)) {
+ return { status: "error", message: RATE_LIMITED_MESSAGE };
+ }
+
+ return runAuthFlow(
+ "",
+ Effect.gen(function* () {
+ const response = yield* authenticateResolvingOrgSelection(() =>
+ getWorkOS().userManagement.authenticateWithTotp({
+ clientId: getWorkOSClientId(),
+ code: parsed.data.code,
+ pendingAuthenticationToken: parsed.data.pendingAuthenticationToken,
+ authenticationChallengeId: parsed.data.authenticationChallengeId,
+ })
+ );
+
+ const session = yield* completeAuthentication(
+ response,
+ parsed.data.returnTo,
+ POSTHOG_EVENTS.MFA_VERIFIED
+ );
+ yield* Effect.promise(clearMfaAttemptCookie);
+ yield* Effect.promise(clearAllPendingMfaFlows);
+
+ const alreadyHasCodes = yield* attemptAfterSignIn(
+ () => hasBackupCodes(session.localUserId),
+ "checking backup codes"
+ );
+ if (alreadyHasCodes !== false) {
+ return signedIn(session);
+ }
+
+ const backupCodes = yield* attemptAfterSignIn(
+ () => replaceBackupCodes(session.localUserId),
+ "issuing backup codes"
+ );
+ if (!backupCodes) {
+ return signedIn(session);
+ }
+ return {
+ status: "enrolled" as const,
+ redirectTo: session.redirectTo,
+ backupCodes,
+ };
+ })
+ );
+}
+
+export async function redeemBackupCodeAction(
+ rawInput: RedeemBackupCodeInput
+): Promise {
+ const parsed = redeemBackupCodeInputSchema.safeParse(rawInput);
+ if (!parsed.success) {
+ return {
+ status: "error",
+ message: parsed.error.issues[0]?.message ?? "Invalid backup code",
+ };
+ }
+
+ const attempt = await readMatchingAttempt(
+ parsed.data.authenticationChallengeId
+ );
+ if (!attempt) {
+ return { status: "error", message: ATTEMPT_EXPIRED_MESSAGE };
+ }
+ const { workosUserId, startedAt } = attempt;
+
+ if (await isAccountRateLimited(ratelimit.backupCode, workosUserId)) {
+ return { status: "error", message: RATE_LIMITED_MESSAGE };
+ }
+
+ const rejected: RedeemBackupCodeResult = {
+ status: "error",
+ message: BACKUP_CODE_REJECTED_MESSAGE,
+ };
+
+ return Effect.runPromise(
+ Effect.gen(function* () {
+ const localUser = yield* Effect.promise(() =>
+ db.query.users.findFirst({
+ where: eq(users.workosUserId, workosUserId),
+ columns: { id: true, email: true },
+ })
+ );
+ if (!localUser) {
+ return rejected;
+ }
+
+ const unused = yield* Effect.promise(() =>
+ hasUnusedBackupCode(localUser.id, parsed.data.code)
+ );
+ if (!unused) {
+ return rejected;
+ }
+
+ const removeLockedOutFactors = Effect.gen(function* () {
+ const factors = yield* tryWorkOSAuth(() =>
+ getWorkOS().multiFactorAuth.listUserAuthFactors({
+ userId: workosUserId,
+ })
+ );
+ const totpFactors = factors.data.filter(
+ (factor) => factor.type === TOTP_FACTOR_TYPE
+ );
+ const lockedOutFactors = totpFactors.filter(
+ (factor) => Date.parse(factor.createdAt) <= startedAt
+ );
+ yield* Effect.forEach(
+ lockedOutFactors,
+ (factor) =>
+ tryWorkOSAuth(() =>
+ getWorkOS().multiFactorAuth.deleteFactor(factor.id)
+ ),
+ { discard: true }
+ );
+ return lockedOutFactors.length === totpFactors.length;
+ });
+ const allFactorsRemoved = yield* removeLockedOutFactors;
+ yield* Effect.promise(() =>
+ consumeBackupCode(localUser.id, parsed.data.code)
+ );
+
+ if (allFactorsRemoved) {
+ yield* Effect.promise(() => clearBackupCodes(localUser.id));
+ }
+ yield* Effect.promise(clearMfaAttemptCookie);
+ yield* Effect.promise(() =>
+ trackAuthEvent(
+ POSTHOG_EVENTS.MFA_BACKUP_CODE_USED,
+ { method: ANALYTICS_AUTH_METHODS.PASSWORD },
+ localUser.id
+ )
+ );
+ return {
+ status: "recovered" as const,
+ email: localUser.email,
+ };
+ }).pipe(
+ Effect.catch((error) =>
+ Effect.succeed({
+ status: "error",
+ message: readWorkOSError(error.error).message,
+ })
+ )
+ )
+ );
+}
+
+export async function resumeSocialEnrollmentAction(
+ rawInput: ResumeSocialEnrollmentInput
+): Promise {
+ const parsed = resumeSocialEnrollmentInputSchema.safeParse(rawInput);
+ if (!parsed.success) {
+ return { status: "error", message: ATTEMPT_EXPIRED_MESSAGE };
+ }
+
+ const flow = await readPendingMfaFlow(parsed.data.flowId);
+ if (flow?.kind !== "enrollment") {
+ return { status: "error", message: ATTEMPT_EXPIRED_MESSAGE };
+ }
+ if (
+ await isAccountRateLimited(
+ ratelimit.signIn,
+ `enrollment:${flow.workosUserId}`
+ )
+ ) {
+ return { status: "error", message: RATE_LIMITED_MESSAGE };
+ }
+ await clearPendingMfaFlow(parsed.data.flowId);
+
+ return runAuthFlow(
+ flow.email,
+ Effect.gen(function* () {
+ const enrollment = yield* beginTotpEnrollment(
+ flow.workosUserId,
+ flow.email
+ );
+ yield* Effect.promise(() =>
+ trackAuthEvent(POSTHOG_EVENTS.MFA_ENROLLMENT_REQUIRED, {
+ method: ANALYTICS_AUTH_METHODS.UNKNOWN,
+ })
+ );
+ const result: AuthFlowResult = {
+ status: "mfa-enrollment-required",
+ pendingAuthenticationToken: flow.pendingAuthenticationToken,
+ email: flow.email,
+ ...enrollment,
+ };
+ return result;
+ })
+ );
+}
diff --git a/apps/dashboard/src/lib/auth/mfa-cookies.ts b/apps/dashboard/src/lib/auth/mfa-cookies.ts
new file mode 100644
index 000000000..baa595625
--- /dev/null
+++ b/apps/dashboard/src/lib/auth/mfa-cookies.ts
@@ -0,0 +1,120 @@
+import "zod/compile";
+// biome-ignore lint/performance/noNamespaceImport: Zod recommended way to import
+import * as z from "zod";
+
+import {
+ MFA_ATTEMPT_COOKIE,
+ MFA_COOKIE_MAX_AGE_SECONDS,
+ MFA_PENDING_COOKIE_PREFIX,
+ TOTP_ENROLLMENT_COOKIE,
+} from "@/constants/security";
+import {
+ clearSignedCookie,
+ clearSignedCookiesWithPrefix,
+ readSignedCookie,
+ storeSignedCookie,
+} from "@/lib/auth/signed-cookie";
+import type {
+ MfaAttempt,
+ PendingMfaFlow,
+ TotpEnrollmentInProgress,
+} from "@/types/auth/mfa-cookies";
+
+const mfaAttemptSchema = z.object({
+ workosUserId: z.string().min(1),
+ authenticationChallengeId: z.string().min(1),
+ startedAt: z.number().int().positive(),
+});
+
+const pendingMfaFlowSchema = z.discriminatedUnion("kind", [
+ z.object({
+ kind: z.literal("challenge"),
+ pendingAuthenticationToken: z.string().min(1),
+ authenticationChallengeId: z.string().min(1),
+ email: z.string(),
+ }),
+ z.object({
+ kind: z.literal("enrollment"),
+ pendingAuthenticationToken: z.string().min(1),
+ workosUserId: z.string().min(1),
+ email: z.string(),
+ }),
+]);
+
+const totpEnrollmentInProgressSchema = z.object({
+ localUserId: z.string().min(1),
+ factorId: z.string().min(1),
+ authenticationChallengeId: z.string().min(1),
+});
+
+const mfaFlowIdSchema = z.uuid();
+
+function getPendingMfaCookieName(flowId: string) {
+ const parsed = mfaFlowIdSchema.safeParse(flowId);
+ return parsed.success ? `${MFA_PENDING_COOKIE_PREFIX}_${parsed.data}` : null;
+}
+
+export function storeMfaAttempt(attempt: MfaAttempt) {
+ return storeSignedCookie(
+ MFA_ATTEMPT_COOKIE,
+ attempt,
+ MFA_COOKIE_MAX_AGE_SECONDS
+ );
+}
+
+export function readMfaAttempt() {
+ return readSignedCookie(MFA_ATTEMPT_COOKIE, mfaAttemptSchema);
+}
+
+export function clearMfaAttemptCookie() {
+ return clearSignedCookie(MFA_ATTEMPT_COOKIE);
+}
+
+export async function storePendingMfaFlow(flow: PendingMfaFlow) {
+ const flowId = crypto.randomUUID();
+ await storeSignedCookie(
+ `${MFA_PENDING_COOKIE_PREFIX}_${flowId}`,
+ flow,
+ MFA_COOKIE_MAX_AGE_SECONDS
+ );
+ return flowId;
+}
+
+export function readPendingMfaFlow(flowId: string) {
+ const cookieName = getPendingMfaCookieName(flowId);
+ return cookieName
+ ? readSignedCookie(cookieName, pendingMfaFlowSchema)
+ : Promise.resolve(null);
+}
+
+export function clearAllPendingMfaFlows() {
+ return clearSignedCookiesWithPrefix(`${MFA_PENDING_COOKIE_PREFIX}_`);
+}
+
+export async function clearPendingMfaFlow(flowId: string) {
+ const cookieName = getPendingMfaCookieName(flowId);
+ if (cookieName) {
+ await clearSignedCookie(cookieName);
+ }
+}
+
+export function storeTotpEnrollmentInProgress(
+ enrollment: TotpEnrollmentInProgress
+) {
+ return storeSignedCookie(
+ TOTP_ENROLLMENT_COOKIE,
+ enrollment,
+ MFA_COOKIE_MAX_AGE_SECONDS
+ );
+}
+
+export function readTotpEnrollmentInProgress() {
+ return readSignedCookie(
+ TOTP_ENROLLMENT_COOKIE,
+ totpEnrollmentInProgressSchema
+ );
+}
+
+export function clearTotpEnrollmentInProgress() {
+ return clearSignedCookie(TOTP_ENROLLMENT_COOKIE);
+}
diff --git a/apps/dashboard/src/lib/auth/mfa.ts b/apps/dashboard/src/lib/auth/mfa.ts
new file mode 100644
index 000000000..87030f99f
--- /dev/null
+++ b/apps/dashboard/src/lib/auth/mfa.ts
@@ -0,0 +1,109 @@
+import { db } from "@notra/db/drizzle";
+import { users } from "@notra/db/schema";
+import type { AuthFlowResult } from "@notra/schemas/types/dashboard/auth";
+import { getWorkOS } from "@workos-inc/authkit-nextjs";
+import { eq } from "drizzle-orm";
+import { Effect } from "effect";
+
+import { MFA_ERROR_CODES, TOTP_FACTOR_TYPE } from "@/constants/security";
+import { clearBackupCodes } from "@/lib/auth/backup-codes";
+import { WorkOSAuthError } from "@/lib/auth/errors";
+import { storeMfaAttempt } from "@/lib/auth/mfa-cookies";
+import { createTotpFactor } from "@/lib/auth/workos-mfa";
+import type { WorkOSErrorInfo } from "@/types/auth/workos-error";
+
+const tryWorkOS = (run: () => Promise) =>
+ Effect.tryPromise({
+ try: run,
+ catch: (error) => new WorkOSAuthError({ error }),
+ });
+
+const createMfaChallenge = Effect.fn("auth.mfa.createChallenge")(function* (
+ authenticationFactorId: string
+) {
+ const challenge = yield* tryWorkOS(() =>
+ getWorkOS().multiFactorAuth.challengeFactor({ authenticationFactorId })
+ );
+ return challenge.id;
+});
+
+const rememberAttempt = (
+ workosUserId: string,
+ authenticationChallengeId: string
+) =>
+ Effect.promise(() =>
+ storeMfaAttempt({
+ workosUserId,
+ authenticationChallengeId,
+ startedAt: Date.now(),
+ })
+ );
+
+async function forgetFactorState(workosUserId: string) {
+ const localUser = await db.query.users.findFirst({
+ where: eq(users.workosUserId, workosUserId),
+ columns: { id: true },
+ });
+ if (!localUser) {
+ return;
+ }
+ await clearBackupCodes(localUser.id);
+}
+
+export const beginTotpEnrollment = Effect.fn("auth.mfa.beginEnrollment")(
+ function* (workosUserId: string, email: string) {
+ yield* Effect.promise(() => forgetFactorState(workosUserId));
+ const enrollment = yield* tryWorkOS(() =>
+ createTotpFactor(workosUserId, email)
+ );
+ yield* rememberAttempt(workosUserId, enrollment.authenticationChallengeId);
+ return enrollment;
+ }
+);
+
+export const resolveMfaFlow = Effect.fn("auth.mfa.resolveFlow")(function* (
+ info: WorkOSErrorInfo,
+ email: string
+) {
+ const pendingAuthenticationToken = info.pendingAuthenticationToken;
+ if (!pendingAuthenticationToken) {
+ return null;
+ }
+
+ const resolvedEmail = email || info.email || "";
+
+ if (info.code === MFA_ERROR_CODES.CHALLENGE) {
+ const factor = info.authenticationFactors.find(
+ (candidate) => candidate.type === TOTP_FACTOR_TYPE
+ );
+
+ if (!factor) {
+ return null;
+ }
+
+ const authenticationChallengeId = yield* createMfaChallenge(factor.id);
+ if (info.userId) {
+ yield* rememberAttempt(info.userId, authenticationChallengeId);
+ }
+ const result: AuthFlowResult = {
+ status: "mfa-required",
+ pendingAuthenticationToken,
+ authenticationChallengeId,
+ email: resolvedEmail,
+ };
+ return result;
+ }
+
+ if (info.code === MFA_ERROR_CODES.ENROLLMENT && info.userId) {
+ const enrollment = yield* beginTotpEnrollment(info.userId, resolvedEmail);
+ const result: AuthFlowResult = {
+ status: "mfa-enrollment-required",
+ pendingAuthenticationToken,
+ email: resolvedEmail,
+ ...enrollment,
+ };
+ return result;
+ }
+
+ return null;
+});
diff --git a/apps/dashboard/src/lib/auth/password-actions.ts b/apps/dashboard/src/lib/auth/password-actions.ts
index 65ae6eb84..02109e69b 100644
--- a/apps/dashboard/src/lib/auth/password-actions.ts
+++ b/apps/dashboard/src/lib/auth/password-actions.ts
@@ -1,7 +1,6 @@
"use server";
-import { POSTHOG_EVENTS, type PostHogEventName } from "@notra/posthog/events";
-import type { PostHogProperties } from "@notra/posthog/types/posthog";
+import { POSTHOG_EVENTS } from "@notra/posthog/events";
import {
forgotPasswordInputSchema,
resetPasswordInputSchema,
@@ -11,168 +10,31 @@ import {
} from "@notra/schemas/dashboard/auth/credentials";
import type {
AuthFlowResult,
+ ForgotPasswordInput,
+ ResetPasswordInput,
SignInWithPasswordInput,
+ SignUpWithPasswordInput,
VerifyEmailCodeInput,
-} from "@notra/ui/lib/auth-types";
-import type { Ratelimit } from "@upstash/ratelimit";
-import { getWorkOS, saveSession } from "@workos-inc/authkit-nextjs";
-import type { AuthenticationResponse } from "@workos-inc/node";
+} from "@notra/schemas/types/dashboard/auth";
+import { getWorkOS } from "@workos-inc/authkit-nextjs";
import { Effect } from "effect";
-import { headers } from "next/headers";
+import { PASSWORD_RESET_OUTCOMES } from "@/constants/analytics-events";
import {
- ANALYTICS_AUTH_METHODS,
- PASSWORD_RESET_OUTCOMES,
-} from "@/constants/analytics-events";
-import { trackServerEvent } from "@/lib/analytics/posthog-server";
-import { readRequestHeaders } from "@/lib/analytics/request-headers";
-import { UserSyncError, WorkOSAuthError } from "@/lib/auth/errors";
+ completeAuthentication,
+ getWorkOSClientId,
+ runAuthFlow,
+ signedIn,
+ trackAuthEvent,
+ tryWorkOSAuth,
+} from "@/lib/auth/auth-flow";
import { authenticateResolvingOrgSelection } from "@/lib/auth/org-selection";
-import { sanitizeReturnTo } from "@/lib/auth/return-to";
-import { syncAuthenticatedUser } from "@/lib/auth/sync";
import { readWorkOSError } from "@/lib/auth/workos-error";
-import type {
- ForgotPasswordInput,
- ResetPasswordInput,
- SignUpWithPasswordInput,
-} from "@/types/auth/password-actions";
-import { getClientIpFromHeaders, ratelimit } from "@/utils/ratelimit";
+import { isRateLimited, ratelimit } from "@/utils/ratelimit";
-async function trackAuthEvent(
- event: PostHogEventName,
- properties?: PostHogProperties,
- userId?: string | null
-) {
- const requestHeaders = await readRequestHeaders();
- trackServerEvent({ event, headers: requestHeaders, userId, properties });
-}
-
-const VERIFICATION_REQUIRED_CODE = "email_verification_required";
const NAME_SPLIT_REGEX = /\s+/;
-const DEFAULT_POST_LOGIN_PATH = "/callback";
const RATE_LIMITED_MESSAGE = "Too many attempts. Please try again shortly.";
-async function isRateLimited(limiter: Ratelimit, email: string) {
- if (
- process.env.NODE_ENV !== "production" &&
- (!process.env.UPSTASH_REDIS_REST_URL ||
- !process.env.UPSTASH_REDIS_REST_TOKEN)
- ) {
- return false;
- }
-
- const headersList = await headers();
- const ip = getClientIpFromHeaders(headersList);
- const { success } = await limiter.limit(`${ip}:${email.toLowerCase()}`);
- return !success;
-}
-
-function getClientId() {
- const clientId = process.env.WORKOS_CLIENT_ID;
- if (!clientId) {
- throw new Error("WORKOS_CLIENT_ID must be defined");
- }
- return clientId;
-}
-
-function getAppUrl() {
- return process.env.APP_URL ?? "http://localhost:3000";
-}
-
-const tryWorkOSAuth = (run: () => Promise) =>
- Effect.tryPromise({
- try: run,
- catch: (error) => new WorkOSAuthError({ error }),
- });
-
-const completeAuthentication = Effect.fn("auth.password.completeSession")(
- function* (
- response: AuthenticationResponse,
- returnTo?: string | null,
- completionEvent?: PostHogEventName
- ) {
- yield* Effect.tryPromise({
- try: () =>
- saveSession(
- {
- accessToken: response.accessToken,
- refreshToken: response.refreshToken,
- user: response.user,
- impersonator: response.impersonator,
- authenticationMethod: response.authenticationMethod,
- },
- getAppUrl()
- ),
- catch: (cause) =>
- new UserSyncError({ message: "Failed to persist session", cause }),
- });
-
- const localUser = yield* syncAuthenticatedUser({
- workosUser: response.user,
- oauthTokens: response.oauthTokens,
- authenticationMethod: response.authenticationMethod,
- });
-
- if (completionEvent) {
- yield* Effect.promise(() =>
- trackAuthEvent(
- completionEvent,
- { method: ANALYTICS_AUTH_METHODS.PASSWORD },
- localUser.id
- )
- );
- }
-
- const redirectTo =
- sanitizeReturnTo(returnTo ?? null) ?? DEFAULT_POST_LOGIN_PATH;
-
- const result: AuthFlowResult = { status: "success", redirectTo };
- return result;
- }
-);
-
-const mapAuthFailure =
- (email: string) =>
- (error: WorkOSAuthError | UserSyncError | { message: string }) => {
- if (error instanceof WorkOSAuthError) {
- const info = readWorkOSError(error.error);
-
- if (
- info.code === VERIFICATION_REQUIRED_CODE &&
- info.pendingAuthenticationToken
- ) {
- const pendingToken = info.pendingAuthenticationToken;
- return Effect.promise(() =>
- trackAuthEvent(POSTHOG_EVENTS.EMAIL_VERIFICATION_REQUIRED, {
- method: ANALYTICS_AUTH_METHODS.PASSWORD,
- })
- ).pipe(
- Effect.as({
- status: "verification-required",
- pendingAuthenticationToken: pendingToken,
- email,
- })
- );
- }
-
- return Effect.succeed({
- status: "error",
- message: info.message,
- });
- }
-
- return Effect.succeed({
- status: "error",
- message: error.message,
- });
- };
-
-const runAuthFlow = (
- email: string,
- flow: Effect.Effect
-): Promise =>
- Effect.runPromise(flow.pipe(Effect.catch(mapAuthFailure(email))));
-
export async function signInWithPasswordAction(
rawInput: SignInWithPasswordInput
): Promise {
@@ -194,13 +56,15 @@ export async function signInWithPasswordAction(
Effect.gen(function* () {
const response = yield* authenticateResolvingOrgSelection(() =>
getWorkOS().userManagement.authenticateWithPassword({
- clientId: getClientId(),
+ clientId: getWorkOSClientId(),
email: parsed.data.email,
password: parsed.data.password,
})
);
- return yield* completeAuthentication(response, parsed.data.returnTo);
+ return signedIn(
+ yield* completeAuthentication(response, parsed.data.returnTo)
+ );
})
);
}
@@ -239,13 +103,15 @@ export async function signUpWithPasswordAction(
const response = yield* authenticateResolvingOrgSelection(() =>
getWorkOS().userManagement.authenticateWithPassword({
- clientId: getClientId(),
+ clientId: getWorkOSClientId(),
email: parsed.data.email,
password: parsed.data.password,
})
);
- return yield* completeAuthentication(response, parsed.data.returnTo);
+ return signedIn(
+ yield* completeAuthentication(response, parsed.data.returnTo)
+ );
})
);
}
@@ -267,16 +133,18 @@ export async function verifyEmailCodeAction(
Effect.gen(function* () {
const response = yield* authenticateResolvingOrgSelection(() =>
getWorkOS().userManagement.authenticateWithEmailVerification({
- clientId: getClientId(),
+ clientId: getWorkOSClientId(),
code: parsed.data.code,
pendingAuthenticationToken: parsed.data.pendingAuthenticationToken,
})
);
- return yield* completeAuthentication(
- response,
- parsed.data.returnTo,
- POSTHOG_EVENTS.EMAIL_VERIFIED
+ return signedIn(
+ yield* completeAuthentication(
+ response,
+ parsed.data.returnTo,
+ POSTHOG_EVENTS.EMAIL_VERIFIED
+ )
);
})
);
@@ -304,19 +172,17 @@ export async function forgotPasswordAction(
}
return Effect.runPromise(
- Effect.gen(function* () {
- yield* tryWorkOSAuth(() =>
- getWorkOS().userManagement.createPasswordReset({ email: input.email })
- );
-
- yield* Effect.promise(() =>
- trackAuthEvent(POSTHOG_EVENTS.PASSWORD_RESET_REQUESTED, {
- outcome: PASSWORD_RESET_OUTCOMES.SENT,
- })
- );
-
- return { sent: true };
- }).pipe(
+ tryWorkOSAuth(() =>
+ getWorkOS().userManagement.createPasswordReset({ email: input.email })
+ ).pipe(
+ Effect.andThen(
+ Effect.promise(() =>
+ trackAuthEvent(POSTHOG_EVENTS.PASSWORD_RESET_REQUESTED, {
+ outcome: PASSWORD_RESET_OUTCOMES.SENT,
+ })
+ )
+ ),
+ Effect.as({ sent: true }),
Effect.catch((error) =>
Effect.logWarning("Password reset request failed").pipe(
Effect.annotateLogs({
diff --git a/apps/dashboard/src/lib/auth/security-actions.ts b/apps/dashboard/src/lib/auth/security-actions.ts
new file mode 100644
index 000000000..0ecbb1953
--- /dev/null
+++ b/apps/dashboard/src/lib/auth/security-actions.ts
@@ -0,0 +1,447 @@
+"use server";
+
+import { POSTHOG_EVENTS, type PostHogEventName } from "@notra/posthog/events";
+import { TOTP_CODE_LENGTH } from "@notra/schemas/constants/dashboard/auth";
+import {
+ discardTotpEnrollmentInputSchema,
+ regenerateBackupCodesInputSchema,
+ removeAuthFactorInputSchema,
+ verifyTotpEnrollmentInputSchema,
+} from "@notra/schemas/dashboard/auth/mfa";
+import type {
+ DiscardTotpEnrollmentInput,
+ RegenerateBackupCodesInput,
+ RegenerateBackupCodesResult,
+ RemoveAuthFactorInput,
+ SecurityOverview,
+ StartTotpEnrollmentResult,
+ TotpFactorSummary,
+ VerifyTotpEnrollmentInput,
+ VerifyTotpEnrollmentResult,
+} from "@notra/schemas/types/dashboard/auth";
+import { normalizeBackupCode } from "@notra/schemas/utils/auth";
+import type { Ratelimit } from "@upstash/ratelimit";
+import { getWorkOS } from "@workos-inc/authkit-nextjs";
+import { Effect } from "effect";
+
+import { SECURITY_ERROR_CODES, TOTP_FACTOR_TYPE } from "@/constants/security";
+import { ActionFailure } from "@/lib/actions/errors";
+import { runAction } from "@/lib/actions/run-action";
+import { validateActionInput } from "@/lib/actions/validate-input";
+import { trackServerEvent } from "@/lib/analytics/posthog-server";
+import { readRequestHeaders } from "@/lib/analytics/request-headers";
+import {
+ clearBackupCodes,
+ consumeBackupCode,
+ countRemainingBackupCodes,
+ hasUnusedBackupCode,
+ replaceBackupCodes,
+} from "@/lib/auth/backup-codes";
+import {
+ clearTotpEnrollmentInProgress,
+ readTotpEnrollmentInProgress,
+ storeTotpEnrollmentInProgress,
+} from "@/lib/auth/mfa-cookies";
+import { readWorkOSError } from "@/lib/auth/workos-error";
+import { createTotpFactor } from "@/lib/auth/workos-mfa";
+import { requireSession } from "@/lib/organizations/guards";
+import type { ActionResult } from "@/types/organizations/actions";
+import { isAccountRateLimited, ratelimit } from "@/utils/ratelimit";
+
+const RATE_LIMITED_MESSAGE = "Too many attempts. Please try again shortly.";
+const INVALID_TOTP_MESSAGE =
+ "That code didn't work. Check your authenticator app and try again.";
+const INVALID_CONFIRMATION_MESSAGE =
+ "That code didn't work. Enter the code from your authenticator app or an unused backup code.";
+const ENROLLMENT_EXPIRED_MESSAGE =
+ "This setup expired or belongs to another session. Start the setup again.";
+const ALREADY_ENABLED_MESSAGE =
+ "Two-factor authentication is already on. Remove the current authenticator app before adding another.";
+
+const tryWorkOS = (run: () => Promise) =>
+ Effect.tryPromise({
+ try: run,
+ catch: (cause) =>
+ new ActionFailure({ message: readWorkOSError(cause).message, cause }),
+ });
+
+const tryDb = (run: () => Promise, message: string) =>
+ Effect.tryPromise({
+ try: run,
+ catch: (cause) => new ActionFailure({ message, cause }),
+ });
+
+const attemptDb = (run: () => Promise) =>
+ Effect.tryPromise(run).pipe(
+ Effect.catch((error) =>
+ Effect.logWarning("Security bookkeeping failed after enrollment").pipe(
+ Effect.annotateLogs({ error: String(error.cause) }),
+ Effect.as(null)
+ )
+ )
+ );
+
+const enforceRateLimit = (limiter: Ratelimit, key: string) =>
+ Effect.promise(() => isAccountRateLimited(limiter, key)).pipe(
+ Effect.andThen((limited) =>
+ limited
+ ? Effect.fail(new ActionFailure({ message: RATE_LIMITED_MESSAGE }))
+ : Effect.void
+ )
+ );
+
+const requireSecurityContext = Effect.fn("auth.security.requireContext")(
+ function* () {
+ const session = yield* requireSession();
+ const workosUserId = session.user.workosUserId;
+ if (!workosUserId) {
+ return yield* Effect.fail(
+ new ActionFailure({
+ code: SECURITY_ERROR_CODES.UNAVAILABLE,
+ message: "Security settings aren't available for this account yet.",
+ })
+ );
+ }
+
+ return {
+ localUserId: session.user.id,
+ email: session.user.email,
+ workosUserId,
+ };
+ }
+);
+
+const trackSecurityEvent = (event: PostHogEventName, userId: string) =>
+ Effect.promise(async () => {
+ const requestHeaders = await readRequestHeaders();
+ trackServerEvent({ event, headers: requestHeaders, userId });
+ });
+
+const listTotpFactors = Effect.fn("auth.security.listTotpFactors")(function* (
+ workosUserId: string
+) {
+ const factors = yield* tryWorkOS(() =>
+ getWorkOS().multiFactorAuth.listUserAuthFactors({
+ userId: workosUserId,
+ })
+ );
+ return factors.data
+ .filter((factor) => factor.type === TOTP_FACTOR_TYPE)
+ .map((factor) => ({
+ id: factor.id,
+ issuer: factor.totp?.issuer ?? null,
+ createdAt: factor.createdAt,
+ }));
+});
+
+interface SecurityContext {
+ localUserId: string;
+ email: string;
+ workosUserId: string;
+}
+
+const isTotpCode = (code: string) =>
+ code.length === TOTP_CODE_LENGTH && /^\d+$/.test(code);
+
+const verifyTotpAgainstFactors = Effect.fn("auth.security.verifyTotp")(
+ function* (factors: TotpFactorSummary[], code: string) {
+ for (const factor of factors) {
+ const challenge = yield* tryWorkOS(() =>
+ getWorkOS().multiFactorAuth.challengeFactor({
+ authenticationFactorId: factor.id,
+ })
+ );
+ const verification = yield* tryWorkOS(() =>
+ getWorkOS().multiFactorAuth.verifyChallenge({
+ authenticationChallengeId: challenge.id,
+ code,
+ })
+ ).pipe(Effect.catch(() => Effect.succeed({ valid: false })));
+ if (verification.valid) {
+ return true;
+ }
+ }
+ return false;
+ }
+);
+
+const confirmSecondFactor = Effect.fn("auth.security.confirmSecondFactor")(
+ function* (
+ context: SecurityContext,
+ factors: TotpFactorSummary[],
+ code: string
+ ) {
+ yield* enforceRateLimit(
+ ratelimit.mfaVerify,
+ `confirm:${context.localUserId}`
+ );
+ if (isTotpCode(code)) {
+ const confirmed = yield* verifyTotpAgainstFactors(factors, code);
+ if (!confirmed) {
+ return yield* Effect.fail(
+ new ActionFailure({
+ code: SECURITY_ERROR_CODES.INVALID_CODE,
+ message: INVALID_CONFIRMATION_MESSAGE,
+ })
+ );
+ }
+ return null;
+ }
+ const backupCode = normalizeBackupCode(code);
+ const unused = yield* tryDb(
+ () => hasUnusedBackupCode(context.localUserId, backupCode),
+ "Couldn't check the backup code. Please try again."
+ );
+ if (!unused) {
+ return yield* Effect.fail(
+ new ActionFailure({
+ code: SECURITY_ERROR_CODES.INVALID_CODE,
+ message: INVALID_CONFIRMATION_MESSAGE,
+ })
+ );
+ }
+ return backupCode;
+ }
+);
+
+const withSecondFactor = (
+ context: SecurityContext,
+ factors: TotpFactorSummary[],
+ code: string,
+ change: Effect.Effect
+) =>
+ Effect.gen(function* () {
+ const backupCode = yield* confirmSecondFactor(context, factors, code);
+ return yield* change.pipe(
+ Effect.tap(() =>
+ backupCode
+ ? Effect.promise(() =>
+ consumeBackupCode(context.localUserId, backupCode)
+ ).pipe(Effect.ignore)
+ : Effect.void
+ )
+ );
+ });
+
+export async function getSecurityOverviewAction(): Promise<
+ ActionResult
+> {
+ return runAction(
+ Effect.gen(function* () {
+ const context = yield* requireSecurityContext();
+
+ const totpFactors = yield* listTotpFactors(context.workosUserId);
+ const backupCodesRemaining =
+ totpFactors.length > 0
+ ? yield* Effect.promise(() =>
+ countRemainingBackupCodes(context.localUserId)
+ )
+ : 0;
+
+ return {
+ email: context.email,
+ totpFactors,
+ backupCodesRemaining,
+ };
+ })
+ );
+}
+
+export async function startTotpEnrollmentAction(): Promise<
+ ActionResult
+> {
+ return runAction(
+ Effect.gen(function* () {
+ const context = yield* requireSecurityContext();
+ const existing = yield* listTotpFactors(context.workosUserId);
+ if (existing.length > 0) {
+ return yield* Effect.fail(
+ new ActionFailure({ message: ALREADY_ENABLED_MESSAGE })
+ );
+ }
+ const enrollment = yield* tryWorkOS(() =>
+ createTotpFactor(context.workosUserId, context.email)
+ );
+ yield* Effect.promise(() =>
+ storeTotpEnrollmentInProgress({
+ localUserId: context.localUserId,
+ factorId: enrollment.factorId,
+ authenticationChallengeId: enrollment.authenticationChallengeId,
+ })
+ );
+ return enrollment;
+ })
+ );
+}
+
+export async function discardTotpEnrollmentAction(
+ rawInput: DiscardTotpEnrollmentInput
+): Promise> {
+ return runAction(
+ Effect.gen(function* () {
+ const context = yield* requireSecurityContext();
+ const input = yield* validateActionInput(
+ discardTotpEnrollmentInputSchema,
+ rawInput
+ );
+ const inProgress = yield* Effect.promise(readTotpEnrollmentInProgress);
+ if (
+ inProgress?.localUserId !== context.localUserId ||
+ inProgress.factorId !== input.factorId
+ ) {
+ return { discarded: false };
+ }
+ yield* tryWorkOS(() =>
+ getWorkOS().multiFactorAuth.deleteFactor(input.factorId)
+ );
+ yield* Effect.promise(clearTotpEnrollmentInProgress);
+ return { discarded: true };
+ })
+ );
+}
+
+export async function verifyTotpEnrollmentAction(
+ rawInput: VerifyTotpEnrollmentInput
+): Promise> {
+ return runAction(
+ Effect.gen(function* () {
+ const context = yield* requireSecurityContext();
+ const input = yield* validateActionInput(
+ verifyTotpEnrollmentInputSchema,
+ rawInput
+ );
+ yield* enforceRateLimit(ratelimit.mfaVerify, context.localUserId);
+
+ const inProgress = yield* Effect.promise(readTotpEnrollmentInProgress);
+ const isOwnEnrollment =
+ inProgress?.localUserId === context.localUserId &&
+ inProgress.factorId === input.factorId &&
+ inProgress.authenticationChallengeId ===
+ input.authenticationChallengeId;
+ if (!isOwnEnrollment) {
+ return yield* Effect.fail(
+ new ActionFailure({ message: ENROLLMENT_EXPIRED_MESSAGE })
+ );
+ }
+
+ const verification = yield* tryWorkOS(() =>
+ getWorkOS().multiFactorAuth.verifyChallenge({
+ authenticationChallengeId: input.authenticationChallengeId,
+ code: input.code,
+ })
+ );
+ if (!verification.valid) {
+ return yield* Effect.fail(
+ new ActionFailure({
+ code: SECURITY_ERROR_CODES.INVALID_CODE,
+ message: INVALID_TOTP_MESSAGE,
+ })
+ );
+ }
+ if (verification.challenge.authenticationFactorId !== input.factorId) {
+ return yield* Effect.fail(
+ new ActionFailure({ message: ENROLLMENT_EXPIRED_MESSAGE })
+ );
+ }
+ yield* Effect.promise(clearTotpEnrollmentInProgress);
+
+ const warnings: string[] = [];
+ const backupCodes = yield* attemptDb(() =>
+ replaceBackupCodes(context.localUserId)
+ );
+ if (backupCodes === null) {
+ warnings.push(
+ "backup codes couldn't be generated. Regenerate them from settings"
+ );
+ }
+ yield* trackSecurityEvent(
+ POSTHOG_EVENTS.MFA_FACTOR_ENROLLED,
+ context.localUserId
+ );
+ return {
+ verified: true as const,
+ backupCodes,
+ warning:
+ warnings.length > 0
+ ? `Two-factor is on, but ${warnings.join(" and ")}.`
+ : null,
+ };
+ })
+ );
+}
+
+export async function regenerateBackupCodesAction(
+ rawInput: RegenerateBackupCodesInput
+): Promise> {
+ return runAction(
+ Effect.gen(function* () {
+ const context = yield* requireSecurityContext();
+ const input = yield* validateActionInput(
+ regenerateBackupCodesInputSchema,
+ rawInput
+ );
+ const factors = yield* listTotpFactors(context.workosUserId);
+ if (factors.length === 0) {
+ return yield* Effect.fail(
+ new ActionFailure({
+ message:
+ "Set up an authenticator app before generating backup codes.",
+ })
+ );
+ }
+ const codes = yield* withSecondFactor(
+ context,
+ factors,
+ input.confirmationCode,
+ tryDb(
+ () => replaceBackupCodes(context.localUserId),
+ "Couldn't generate backup codes. Please try again."
+ )
+ );
+ yield* trackSecurityEvent(
+ POSTHOG_EVENTS.MFA_BACKUP_CODES_REGENERATED,
+ context.localUserId
+ );
+ return { codes };
+ })
+ );
+}
+
+export async function removeAuthFactorAction(
+ rawInput: RemoveAuthFactorInput
+): Promise> {
+ return runAction(
+ Effect.gen(function* () {
+ const context = yield* requireSecurityContext();
+ const input = yield* validateActionInput(
+ removeAuthFactorInputSchema,
+ rawInput
+ );
+
+ const factors = yield* listTotpFactors(context.workosUserId);
+ if (!factors.some((factor) => factor.id === input.factorId)) {
+ return yield* Effect.fail(
+ new ActionFailure({
+ message: "That authentication method no longer exists.",
+ })
+ );
+ }
+ yield* withSecondFactor(
+ context,
+ factors,
+ input.confirmationCode,
+ tryWorkOS(() =>
+ getWorkOS().multiFactorAuth.deleteFactor(input.factorId)
+ )
+ );
+ if (factors.length === 1) {
+ yield* Effect.promise(() => clearBackupCodes(context.localUserId));
+ }
+ yield* trackSecurityEvent(
+ POSTHOG_EVENTS.MFA_FACTOR_REMOVED,
+ context.localUserId
+ );
+ return { removed: true as const };
+ })
+ );
+}
diff --git a/apps/dashboard/src/lib/auth/short-lived-cookie.ts b/apps/dashboard/src/lib/auth/short-lived-cookie.ts
new file mode 100644
index 000000000..d9bc89d11
--- /dev/null
+++ b/apps/dashboard/src/lib/auth/short-lived-cookie.ts
@@ -0,0 +1,39 @@
+import { cookies } from "next/headers";
+
+export async function readShortLivedCookie(
+ name: string
+): Promise {
+ const cookieStore = await cookies();
+ return cookieStore.get(name)?.value || null;
+}
+
+export async function storeShortLivedCookie(
+ name: string,
+ value: string,
+ maxAgeSeconds: number
+) {
+ const cookieStore = await cookies();
+ cookieStore.set({
+ name,
+ value,
+ httpOnly: true,
+ sameSite: "lax",
+ secure: process.env.NODE_ENV === "production",
+ path: "/",
+ maxAge: maxAgeSeconds,
+ });
+}
+
+export async function clearShortLivedCookie(name: string) {
+ const cookieStore = await cookies();
+ cookieStore.delete({ name, path: "/" });
+}
+
+export async function clearShortLivedCookiesWithPrefix(prefix: string) {
+ const cookieStore = await cookies();
+ for (const cookie of cookieStore.getAll()) {
+ if (cookie.name.startsWith(prefix)) {
+ cookieStore.delete({ name: cookie.name, path: "/" });
+ }
+ }
+}
diff --git a/apps/dashboard/src/lib/auth/signed-cookie.ts b/apps/dashboard/src/lib/auth/signed-cookie.ts
new file mode 100644
index 000000000..cf8ac4ac0
--- /dev/null
+++ b/apps/dashboard/src/lib/auth/signed-cookie.ts
@@ -0,0 +1,79 @@
+import { createHmac, timingSafeEqual } from "node:crypto";
+
+import type * as z from "zod";
+
+import {
+ clearShortLivedCookie,
+ clearShortLivedCookiesWithPrefix,
+ readShortLivedCookie,
+ storeShortLivedCookie,
+} from "@/lib/auth/short-lived-cookie";
+
+const SIGNATURE_SEPARATOR = ".";
+const KEY_CONTEXT = "notra-signed-cookie";
+
+function getSigningKey(): Buffer {
+ const password = process.env.WORKOS_COOKIE_PASSWORD;
+ if (!password) {
+ throw new Error("WORKOS_COOKIE_PASSWORD must be defined");
+ }
+ return createHmac("sha256", password).update(KEY_CONTEXT).digest();
+}
+
+function sign(encodedPayload: string): string {
+ return createHmac("sha256", getSigningKey())
+ .update(encodedPayload)
+ .digest("base64url");
+}
+
+export async function storeSignedCookie(
+ name: string,
+ payload: unknown,
+ maxAgeSeconds: number
+) {
+ const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
+ await storeShortLivedCookie(
+ name,
+ `${encoded}${SIGNATURE_SEPARATOR}${sign(encoded)}`,
+ maxAgeSeconds
+ );
+}
+
+export async function readSignedCookie(
+ name: string,
+ schema: z.ZodType
+): Promise {
+ const raw = await readShortLivedCookie(name);
+ if (!raw) {
+ return null;
+ }
+ const separatorIndex = raw.lastIndexOf(SIGNATURE_SEPARATOR);
+ if (separatorIndex === -1) {
+ return null;
+ }
+ const encoded = raw.slice(0, separatorIndex);
+ const provided = Buffer.from(raw.slice(separatorIndex + 1), "base64url");
+ const expected = Buffer.from(sign(encoded), "base64url");
+ if (
+ provided.length !== expected.length ||
+ !timingSafeEqual(provided, expected)
+ ) {
+ return null;
+ }
+ try {
+ const parsed = schema.safeParse(
+ JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"))
+ );
+ return parsed.success ? parsed.data : null;
+ } catch {
+ return null;
+ }
+}
+
+export function clearSignedCookie(name: string) {
+ return clearShortLivedCookie(name);
+}
+
+export function clearSignedCookiesWithPrefix(prefix: string) {
+ return clearShortLivedCookiesWithPrefix(prefix);
+}
diff --git a/apps/dashboard/src/lib/auth/social-actions.ts b/apps/dashboard/src/lib/auth/social-actions.ts
index fa6c3e585..af1816d7e 100644
--- a/apps/dashboard/src/lib/auth/social-actions.ts
+++ b/apps/dashboard/src/lib/auth/social-actions.ts
@@ -1,7 +1,7 @@
"use server";
import { startSocialSignInInputSchema } from "@notra/schemas/dashboard/auth/social";
-import type { StartSocialSignInInput } from "@notra/ui/lib/auth-types";
+import type { StartSocialSignInInput } from "@notra/schemas/types/dashboard/auth";
import { getWorkOS } from "@workos-inc/authkit-nextjs";
import { cookies, headers } from "next/headers";
import { redirect } from "next/navigation";
diff --git a/apps/dashboard/src/lib/auth/user-actions.ts b/apps/dashboard/src/lib/auth/user-actions.ts
index 81e784e53..3d25c94df 100644
--- a/apps/dashboard/src/lib/auth/user-actions.ts
+++ b/apps/dashboard/src/lib/auth/user-actions.ts
@@ -12,14 +12,14 @@ import { getWorkOS, signOut, withAuth } from "@workos-inc/authkit-nextjs";
import { and, eq } from "drizzle-orm";
import { Effect } from "effect";
+import { ActionFailure } from "@/lib/actions/errors";
+import { runAction } from "@/lib/actions/run-action";
+import { validateActionInput } from "@/lib/actions/validate-input";
import { trackServerEvent } from "@/lib/analytics/posthog-server";
import { readRequestHeaders } from "@/lib/analytics/request-headers";
import { clearAuthSessionCookie } from "@/lib/auth/session-cookie";
import { isWorkOSNotFound } from "@/lib/auth/workos-error";
-import { OrganizationActionError } from "@/lib/organizations/errors";
import { requireSession } from "@/lib/organizations/guards";
-import { runOrganizationAction } from "@/lib/organizations/run-action";
-import { validateActionInput } from "@/lib/organizations/validate-input";
import type { SessionUser } from "@/types/auth/session";
import type {
SignOutActionOptions,
@@ -31,7 +31,7 @@ import type { AccountInfo, ActionResult } from "@/types/organizations/actions";
const tryAction = (run: () => Promise, message: string) =>
Effect.tryPromise({
try: run,
- catch: (cause) => new OrganizationActionError({ message, cause }),
+ catch: (cause) => new ActionFailure({ message, cause }),
});
export async function signOutAction(options?: SignOutActionOptions) {
@@ -42,7 +42,7 @@ export async function signOutAction(options?: SignOutActionOptions) {
export async function updateUserAction(
rawInput: UpdateUserInput
): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
const input = yield* validateActionInput(updateUserInputSchema, rawInput);
@@ -79,7 +79,7 @@ export async function updateUserAction(
if (!updated) {
return yield* Effect.fail(
- new OrganizationActionError({ message: "User not found" })
+ new ActionFailure({ message: "User not found" })
);
}
@@ -110,7 +110,7 @@ export async function updateUserAction(
export async function deleteUserAction(): Promise<
ActionResult<{ deleted: boolean }>
> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
@@ -178,7 +178,7 @@ export async function deleteUserAction(): Promise<
export async function requestPasswordResetAction(): Promise<
ActionResult<{ sent: boolean }>
> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
@@ -198,7 +198,7 @@ export async function requestPasswordResetAction(): Promise<
export async function listAccountsAction(): Promise<
ActionResult
> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
@@ -224,7 +224,7 @@ export async function listAccountsAction(): Promise<
export async function unlinkAccountAction(
rawInput: UnlinkAccountInput
): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
const input = yield* validateActionInput(
diff --git a/apps/dashboard/src/lib/auth/workos-error.ts b/apps/dashboard/src/lib/auth/workos-error.ts
index 0c17aea94..bbad26827 100644
--- a/apps/dashboard/src/lib/auth/workos-error.ts
+++ b/apps/dashboard/src/lib/auth/workos-error.ts
@@ -1,13 +1,7 @@
import { workosErrorSchema } from "@notra/schemas/dashboard/auth/workos-error";
import { NotFoundException } from "@workos-inc/node";
-export interface WorkOSErrorInfo {
- code: string | null;
- message: string;
- email: string | null;
- pendingAuthenticationToken: string | null;
- organizationIds: string[];
-}
+import type { WorkOSErrorInfo } from "@/types/auth/workos-error";
export function readWorkOSError(error: unknown): WorkOSErrorInfo {
const parsed = workosErrorSchema.safeParse(error);
@@ -19,6 +13,8 @@ export function readWorkOSError(error: unknown): WorkOSErrorInfo {
email: null,
pendingAuthenticationToken: null,
organizationIds: [],
+ authenticationFactors: [],
+ userId: null,
};
}
@@ -27,10 +23,16 @@ export function readWorkOSError(error: unknown): WorkOSErrorInfo {
return {
code: rawData?.code ?? code ?? null,
message: rawData?.message ?? message ?? "Something went wrong",
- email: rawData?.email ?? null,
+ email: rawData?.email ?? rawData?.user?.email ?? null,
pendingAuthenticationToken: rawData?.pending_authentication_token ?? null,
organizationIds:
rawData?.organizations?.map((organization) => organization.id) ?? [],
+ authenticationFactors:
+ rawData?.authentication_factors?.map((factor) => ({
+ id: factor.id,
+ type: factor.type,
+ })) ?? [],
+ userId: rawData?.user?.id ?? null,
};
}
diff --git a/apps/dashboard/src/lib/auth/workos-mfa.ts b/apps/dashboard/src/lib/auth/workos-mfa.ts
new file mode 100644
index 000000000..01cb5c7a5
--- /dev/null
+++ b/apps/dashboard/src/lib/auth/workos-mfa.ts
@@ -0,0 +1,24 @@
+import type { TotpEnrollment } from "@notra/schemas/types/dashboard/auth";
+import { getWorkOS } from "@workos-inc/authkit-nextjs";
+
+import { TOTP_FACTOR_TYPE, TOTP_ISSUER } from "@/constants/security";
+
+export async function createTotpFactor(
+ userId: string,
+ totpUser: string
+): Promise {
+ const enrollment = await getWorkOS().multiFactorAuth.createUserAuthFactor({
+ userId,
+ type: TOTP_FACTOR_TYPE,
+ totpIssuer: TOTP_ISSUER,
+ totpUser,
+ });
+
+ return {
+ factorId: enrollment.authenticationFactor.id,
+ authenticationChallengeId: enrollment.authenticationChallenge.id,
+ qrCode: enrollment.authenticationFactor.totp.qrCode,
+ secret: enrollment.authenticationFactor.totp.secret,
+ otpauthUri: enrollment.authenticationFactor.totp.uri,
+ };
+}
diff --git a/apps/dashboard/src/lib/organizations/actions.ts b/apps/dashboard/src/lib/organizations/actions.ts
index 998a0ad8c..198638881 100644
--- a/apps/dashboard/src/lib/organizations/actions.ts
+++ b/apps/dashboard/src/lib/organizations/actions.ts
@@ -34,21 +34,21 @@ import {
LAST_VISITED_ORGANIZATION_COOKIE,
LAST_VISITED_ORGANIZATION_COOKIE_MAX_AGE,
} from "@/constants/cookies";
+import { ActionFailure } from "@/lib/actions/errors";
+import { runAction } from "@/lib/actions/run-action";
+import { validateActionInput } from "@/lib/actions/validate-input";
import {
identifyOrganizationGroup,
trackServerEvent,
} from "@/lib/analytics/posthog-server";
import { readRequestHeaders } from "@/lib/analytics/request-headers";
import { readWorkOSError } from "@/lib/auth/workos-error";
-import { OrganizationActionError } from "@/lib/organizations/errors";
import {
requireManagerMembership,
requireMembership,
requireSession,
resolveOrganizationId,
} from "@/lib/organizations/guards";
-import { runOrganizationAction } from "@/lib/organizations/run-action";
-import { validateActionInput } from "@/lib/organizations/validate-input";
import {
ensureWorkOSOrganizationWithMembers,
removeMembershipFromWorkOS,
@@ -79,7 +79,7 @@ const enforceTeamMembersLimit = Effect.fn(
const status = yield* Effect.tryPromise({
try: () => checkTeamMembersLimit(organizationId),
catch: (cause) =>
- new OrganizationActionError({
+ new ActionFailure({
message: TEAM_MEMBER_LIMIT_CHECK_UNAVAILABLE_MESSAGE,
cause,
}),
@@ -87,7 +87,7 @@ const enforceTeamMembersLimit = Effect.fn(
if (status === "check-unavailable") {
return yield* Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
message: TEAM_MEMBER_LIMIT_CHECK_UNAVAILABLE_MESSAGE,
})
);
@@ -95,7 +95,7 @@ const enforceTeamMembersLimit = Effect.fn(
if (status === "limit-reached") {
return yield* Effect.fail(
- new OrganizationActionError({ message: TEAM_MEMBER_LIMIT_ERROR_MESSAGE })
+ new ActionFailure({ message: TEAM_MEMBER_LIMIT_ERROR_MESSAGE })
);
}
});
@@ -103,7 +103,7 @@ const enforceTeamMembersLimit = Effect.fn(
const tryDb = (run: () => Promise, message: string) =>
Effect.tryPromise({
try: run,
- catch: (cause) => new OrganizationActionError({ message, cause }),
+ catch: (cause) => new ActionFailure({ message, cause }),
});
const trackOrganizationEvent = Effect.fn(
@@ -172,7 +172,7 @@ const tryWorkOS = (run: () => Promise, fallbackMessage: string) =>
Effect.tryPromise({
try: run,
catch: (cause) =>
- new OrganizationActionError({
+ new ActionFailure({
message: readWorkOSError(cause).message || fallbackMessage,
cause,
}),
@@ -194,7 +194,7 @@ const requireWorkOSOrganizationId = Effect.fn(
return yield* ensureWorkOSOrganizationWithMembers(organizationId).pipe(
Effect.catch((error) =>
Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
message: "This organization is not linked to WorkOS yet",
cause: error,
})
@@ -215,7 +215,7 @@ const requireInvitationManagement = Effect.fn(
if (!invitation.organizationId) {
return yield* Effect.fail(
- new OrganizationActionError({ message: "Invitation not found" })
+ new ActionFailure({ message: "Invitation not found" })
);
}
@@ -230,7 +230,7 @@ const requireInvitationManagement = Effect.fn(
if (!organization) {
return yield* Effect.fail(
- new OrganizationActionError({ message: "Organization not found" })
+ new ActionFailure({ message: "Organization not found" })
);
}
@@ -245,7 +245,7 @@ const requireInvitationManagement = Effect.fn(
export async function createOrganizationAction(
rawInput: CreateOrganizationInput
): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
const input = yield* validateActionInput(
@@ -265,7 +265,7 @@ export async function createOrganizationAction(
if (existing) {
return yield* Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
message: "An organization with this slug already exists",
})
);
@@ -303,7 +303,7 @@ export async function createOrganizationAction(
if (!organization) {
return yield* Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
message: "Organization creation returned no row",
})
);
@@ -320,7 +320,7 @@ export async function createOrganizationAction(
).pipe(
Effect.andThen(
Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
message: "Failed to link organization to WorkOS",
cause: error,
})
@@ -333,7 +333,7 @@ export async function createOrganizationAction(
yield* Effect.tryPromise({
try: () => seedSystemSkills(organizationId),
catch: (cause) =>
- new OrganizationActionError({
+ new ActionFailure({
message: "Failed to seed system skills",
cause,
}),
@@ -355,7 +355,7 @@ export async function createOrganizationAction(
metadata: { orgId: organizationId },
}),
catch: (cause) =>
- new OrganizationActionError({
+ new ActionFailure({
message: "Failed to create billing customer",
cause,
}),
@@ -405,7 +405,7 @@ export async function createOrganizationAction(
export async function updateOrganizationAction(
rawInput: UpdateOrganizationInput
): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
const input = yield* validateActionInput(
@@ -444,7 +444,7 @@ export async function updateOrganizationAction(
if (!organization) {
return yield* Effect.fail(
- new OrganizationActionError({ message: "Organization not found" })
+ new ActionFailure({ message: "Organization not found" })
);
}
@@ -487,7 +487,7 @@ export async function updateOrganizationAction(
export async function listOrganizationsAction(): Promise<
ActionResult
> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
@@ -527,7 +527,7 @@ function findOrganizationForSelection(input: SetActiveOrganizationInput) {
export async function setActiveOrganizationAction(
rawInput: SetActiveOrganizationInput
): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
const input = yield* validateActionInput(
@@ -542,7 +542,7 @@ export async function setActiveOrganizationAction(
if (!organization) {
return yield* Effect.fail(
- new OrganizationActionError({ message: "Organization not found" })
+ new ActionFailure({ message: "Organization not found" })
);
}
@@ -562,15 +562,10 @@ export async function setActiveOrganizationAction(
);
}
-/**
- * Organization row for a `/[slug]` route, without the member join. Unlike
- * `validateOrganizationAccess` this never redirects, so it is safe to call from
- * a client query.
- */
export async function getOrganizationSummaryAction(
rawSlug: string
): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
const slug = yield* validateActionInput(
@@ -588,7 +583,7 @@ export async function getOrganizationSummaryAction(
if (!organization) {
return yield* Effect.fail(
- new OrganizationActionError({ message: "Organization not found" })
+ new ActionFailure({ message: "Organization not found" })
);
}
@@ -602,7 +597,7 @@ export async function getOrganizationSummaryAction(
export async function getFullOrganizationAction(rawInput?: {
query?: { organizationId?: string; organizationSlug?: string };
}): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
const input = yield* validateActionInput(
@@ -673,7 +668,7 @@ export async function getFullOrganizationAction(rawInput?: {
export async function listMembersAction(
rawInput?: ListMembersInput
): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
const input = yield* validateActionInput(
@@ -709,7 +704,7 @@ export async function listMembersAction(
export async function updateMemberRoleAction(
rawInput: UpdateMemberRoleInput
): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
const input = yield* validateActionInput(
@@ -727,7 +722,7 @@ export async function updateMemberRoleAction(
if (!member) {
return yield* Effect.fail(
- new OrganizationActionError({ message: "Member not found" })
+ new ActionFailure({ message: "Member not found" })
);
}
@@ -738,7 +733,7 @@ export async function updateMemberRoleAction(
if (input.role === "owner" && callerMembership.role !== "owner") {
return yield* Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
message: "Only the organization owner can assign the owner role",
})
);
@@ -746,7 +741,7 @@ export async function updateMemberRoleAction(
if (member.role === "owner" && input.role !== "owner") {
return yield* Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
message: "The organization owner role cannot be changed",
})
);
@@ -802,7 +797,7 @@ export async function updateMemberRoleAction(
export async function removeMemberAction(
rawInput: RemoveMemberInput
): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
const input = yield* validateActionInput(
@@ -846,7 +841,7 @@ export async function removeMemberAction(
if (!member) {
return yield* Effect.fail(
- new OrganizationActionError({ message: "Member not found" })
+ new ActionFailure({ message: "Member not found" })
);
}
@@ -858,7 +853,7 @@ export async function removeMemberAction(
if (member.role === "owner") {
return yield* Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
message: "The organization owner cannot be removed",
})
);
@@ -891,7 +886,7 @@ export async function removeMemberAction(
export async function listInvitationsAction(rawInput?: {
query?: { organizationId?: string };
}): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
const input = yield* validateActionInput(
@@ -922,7 +917,7 @@ export async function listInvitationsAction(rawInput?: {
export async function inviteMemberAction(
rawInput: InviteMemberInput
): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const session = yield* requireSession();
const input = yield* validateActionInput(
@@ -940,7 +935,7 @@ export async function inviteMemberAction(
if (input.role === "owner" && callerMembership.role !== "owner") {
return yield* Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
message: "Only the organization owner can assign the owner role",
})
);
@@ -948,7 +943,7 @@ export async function inviteMemberAction(
if (!isNotDisposableEmail(input.email)) {
return yield* Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
message: "Disposable email addresses are not allowed",
})
);
@@ -1018,7 +1013,7 @@ export async function inviteMemberAction(
export async function cancelInvitationAction(
rawInput: InvitationActionInput
): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const input = yield* validateActionInput(
invitationActionInputSchema,
@@ -1046,7 +1041,7 @@ export async function cancelInvitationAction(
export async function resendInvitationAction(
rawInput: InvitationActionInput
): Promise> {
- return runOrganizationAction(
+ return runAction(
Effect.gen(function* () {
const input = yield* validateActionInput(
invitationActionInputSchema,
diff --git a/apps/dashboard/src/lib/organizations/errors.ts b/apps/dashboard/src/lib/organizations/errors.ts
index 09e4382e0..1b7fe3886 100644
--- a/apps/dashboard/src/lib/organizations/errors.ts
+++ b/apps/dashboard/src/lib/organizations/errors.ts
@@ -1,12 +1,5 @@
import { Data } from "effect";
-export class OrganizationActionError extends Data.TaggedError(
- "OrganizationActionError"
-)<{
- readonly message: string;
- readonly cause?: unknown;
-}> {}
-
export class WorkOSSyncError extends Data.TaggedError("WorkOSSyncError")<{
readonly message: string;
readonly cause: unknown;
diff --git a/apps/dashboard/src/lib/organizations/guards.ts b/apps/dashboard/src/lib/organizations/guards.ts
index 831914806..e3411ec14 100644
--- a/apps/dashboard/src/lib/organizations/guards.ts
+++ b/apps/dashboard/src/lib/organizations/guards.ts
@@ -3,8 +3,8 @@ import { members } from "@notra/db/schema";
import { and, eq } from "drizzle-orm";
import { Effect } from "effect";
+import { ActionFailure } from "@/lib/actions/errors";
import { getAuthSession } from "@/lib/auth/server";
-import { OrganizationActionError } from "@/lib/organizations/errors";
import type { AuthSessionData } from "@/types/auth/session";
const MANAGER_ROLES: readonly string[] = ["owner", "admin"];
@@ -14,16 +14,14 @@ export const requireSession = Effect.fn("organizations.guards.requireSession")(
const session = yield* Effect.tryPromise({
try: () => getAuthSession(),
catch: (cause) =>
- new OrganizationActionError({
+ new ActionFailure({
message: "Failed to load session",
cause,
}),
});
if (!session) {
- return yield* Effect.fail(
- new OrganizationActionError({ message: "Unauthorized" })
- );
+ return yield* Effect.fail(new ActionFailure({ message: "Unauthorized" }));
}
return session;
@@ -42,7 +40,7 @@ export const requireMembership = Effect.fn(
),
}),
catch: (cause) =>
- new OrganizationActionError({
+ new ActionFailure({
message: "Failed to check membership",
cause,
}),
@@ -50,7 +48,7 @@ export const requireMembership = Effect.fn(
if (!membership) {
return yield* Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
message: "You are not a member of this organization",
})
);
@@ -66,7 +64,7 @@ export const requireManagerMembership = Effect.fn(
if (!MANAGER_ROLES.includes(membership.role)) {
return yield* Effect.fail(
- new OrganizationActionError({
+ new ActionFailure({
message: "You do not have permission to manage this organization",
})
);
@@ -82,7 +80,7 @@ export const resolveOrganizationId = Effect.fn(
if (!resolved) {
return yield* Effect.fail(
- new OrganizationActionError({ message: "No active organization" })
+ new ActionFailure({ message: "No active organization" })
);
}
diff --git a/apps/dashboard/src/types/auth/login-form.ts b/apps/dashboard/src/types/auth/login-form.ts
index dbbc847df..8d8c0fe90 100644
--- a/apps/dashboard/src/types/auth/login-form.ts
+++ b/apps/dashboard/src/types/auth/login-form.ts
@@ -1,4 +1,4 @@
-import type { LoginFormProps as SharedLoginFormProps } from "@notra/ui/lib/auth-types";
+import type { LoginFormProps as SharedLoginFormProps } from "@notra/ui/types/auth";
export type LoginFormProps = Omit<
SharedLoginFormProps,
@@ -6,5 +6,7 @@ export type LoginFormProps = Omit<
| "validators"
| "signInWithPassword"
| "verifyEmailCode"
+ | "verifyMfaCode"
+ | "redeemBackupCode"
| "startSocialSignIn"
>;
diff --git a/apps/dashboard/src/types/auth/login-page.ts b/apps/dashboard/src/types/auth/login-page.ts
new file mode 100644
index 000000000..9259dfbe9
--- /dev/null
+++ b/apps/dashboard/src/types/auth/login-page.ts
@@ -0,0 +1,15 @@
+import type { PendingAuthStep } from "@notra/schemas/types/dashboard/auth";
+
+export interface LoginPageProps {
+ searchParams: Promise>;
+}
+
+export interface LoginPageStart {
+ pending?: PendingAuthStep;
+ resumeEnrollmentFlowId?: string;
+}
+
+export interface SocialEnrollmentResumeProps {
+ flowId: string;
+ returnTo?: string;
+}
diff --git a/apps/dashboard/src/types/auth/mfa-cookies.ts b/apps/dashboard/src/types/auth/mfa-cookies.ts
new file mode 100644
index 000000000..2b67a5de5
--- /dev/null
+++ b/apps/dashboard/src/types/auth/mfa-cookies.ts
@@ -0,0 +1,27 @@
+export interface MfaAttempt {
+ workosUserId: string;
+ authenticationChallengeId: string;
+ startedAt: number;
+}
+
+export interface PendingMfaChallenge {
+ kind: "challenge";
+ pendingAuthenticationToken: string;
+ authenticationChallengeId: string;
+ email: string;
+}
+
+export interface PendingMfaEnrollment {
+ kind: "enrollment";
+ pendingAuthenticationToken: string;
+ workosUserId: string;
+ email: string;
+}
+
+export type PendingMfaFlow = PendingMfaChallenge | PendingMfaEnrollment;
+
+export interface TotpEnrollmentInProgress {
+ localUserId: string;
+ factorId: string;
+ authenticationChallengeId: string;
+}
diff --git a/apps/dashboard/src/types/auth/password-actions.ts b/apps/dashboard/src/types/auth/password-actions.ts
deleted file mode 100644
index cf170f7a3..000000000
--- a/apps/dashboard/src/types/auth/password-actions.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-export interface SignUpWithPasswordInput {
- email: string;
- password: string;
- name?: string;
- returnTo?: string | null;
-}
-
-export interface ForgotPasswordInput {
- email: string;
-}
-
-export interface ResetPasswordInput {
- token: string;
- newPassword: string;
-}
diff --git a/apps/dashboard/src/types/auth/workos-error.ts b/apps/dashboard/src/types/auth/workos-error.ts
new file mode 100644
index 000000000..2d6f44b64
--- /dev/null
+++ b/apps/dashboard/src/types/auth/workos-error.ts
@@ -0,0 +1,14 @@
+export interface WorkOSAuthenticationFactorRef {
+ id: string;
+ type: string;
+}
+
+export interface WorkOSErrorInfo {
+ code: string | null;
+ message: string;
+ email: string | null;
+ pendingAuthenticationToken: string | null;
+ organizationIds: string[];
+ authenticationFactors: WorkOSAuthenticationFactorRef[];
+ userId: string | null;
+}
diff --git a/apps/dashboard/src/types/design-system/auth-flow.ts b/apps/dashboard/src/types/design-system/auth-flow.ts
new file mode 100644
index 000000000..ab39e08bd
--- /dev/null
+++ b/apps/dashboard/src/types/design-system/auth-flow.ts
@@ -0,0 +1,55 @@
+export interface DevAccount {
+ email: string;
+ password: string;
+ totpSecret: string | null;
+ totpEnrolledAt: string | null;
+}
+
+export interface DevSession {
+ email: string;
+ secondFactor: "totp" | null;
+ signedInAt: string;
+}
+
+export interface DevPendingAuth {
+ token: string;
+ challengeId: string;
+ kind: "mfa" | "enrollment";
+ enrollmentSecret: string | null;
+}
+
+export interface DevLogEntry {
+ id: string;
+ at: string;
+ message: string;
+}
+
+export interface DevSettingsEnrollment {
+ secret: string;
+ qrCode: string;
+ otpauthUri: string;
+}
+
+export type AuthFlowTab = "sign-in" | "settings";
+
+export interface AuthenticatorWidgetProps {
+ secret: string | null;
+}
+
+export interface SimulatorPanelProps {
+ account: DevAccount;
+ backupCodeCount: number;
+ orgRequiresMfa: boolean;
+ session: DevSession | null;
+ pending: DevPendingAuth | null;
+ settingsEnrollmentSecret: string | null;
+ log: DevLogEntry[];
+ onToggleOrgRequiresMfa: (value: boolean) => void;
+ onReset: () => void;
+}
+
+export interface SignedInViewProps {
+ session: DevSession;
+ onSignOut: () => void;
+ onOpenSettings: () => void;
+}
diff --git a/apps/dashboard/src/types/organizations/actions.ts b/apps/dashboard/src/types/organizations/actions.ts
index 87b79b1a3..41b1de4b3 100644
--- a/apps/dashboard/src/types/organizations/actions.ts
+++ b/apps/dashboard/src/types/organizations/actions.ts
@@ -6,12 +6,12 @@ import type {
export interface ActionError {
message: string;
+ code?: string;
}
-export interface ActionResult {
- data: T | null;
- error: ActionError | null;
-}
+export type ActionResult =
+ | { data: T; error: null }
+ | { data: null; error: ActionError };
export type OrganizationRow = typeof organizations.$inferSelect;
export type MemberRow = typeof members.$inferSelect;
diff --git a/apps/dashboard/src/types/settings/security.ts b/apps/dashboard/src/types/settings/security.ts
new file mode 100644
index 000000000..9ccc5a93f
--- /dev/null
+++ b/apps/dashboard/src/types/settings/security.ts
@@ -0,0 +1,19 @@
+import type { TotpFactorSummary } from "@notra/schemas/types/dashboard/auth";
+import type { SecurityLoadStatus } from "@notra/ui/types/security";
+
+export interface TwoFactorSectionProps {
+ accountLabel: string;
+ factors: TotpFactorSummary[];
+ backupCodesRemaining: number | null;
+ status: SecurityLoadStatus;
+ onRefresh: () => Promise | void;
+}
+
+export interface ActiveTotpEnrollment {
+ kind: "scanning" | "verified";
+ factorId: string;
+ authenticationChallengeId: string;
+ qrCode: string;
+ secret: string;
+ otpauthUri: string;
+}
diff --git a/apps/dashboard/src/utils/design-system-qr.ts b/apps/dashboard/src/utils/design-system-qr.ts
new file mode 100644
index 000000000..7730f4e5c
--- /dev/null
+++ b/apps/dashboard/src/utils/design-system-qr.ts
@@ -0,0 +1,40 @@
+const DEMO_QR_MODULES = 21;
+const DEMO_QR_SCALE = 8;
+const DEMO_FINDER_SIZE = 7;
+
+function isFinderModule(x: number, y: number) {
+ const inFinder = (originX: number, originY: number) => {
+ const dx = x - originX;
+ const dy = y - originY;
+ if (dx < 0 || dy < 0 || dx >= DEMO_FINDER_SIZE || dy >= DEMO_FINDER_SIZE) {
+ return null;
+ }
+ const ring = Math.min(
+ dx,
+ dy,
+ DEMO_FINDER_SIZE - 1 - dx,
+ DEMO_FINDER_SIZE - 1 - dy
+ );
+ return ring !== 1;
+ };
+ const offset = DEMO_QR_MODULES - DEMO_FINDER_SIZE;
+ return inFinder(0, 0) ?? inFinder(offset, 0) ?? inFinder(0, offset);
+}
+
+export function buildPlaceholderQrCode(seed = 0) {
+ const rects: string[] = [];
+ for (let y = 0; y < DEMO_QR_MODULES; y += 1) {
+ for (let x = 0; x < DEMO_QR_MODULES; x += 1) {
+ const finder = isFinderModule(x, y);
+ const filled = finder ?? (x * 7 + y * 13 + x * y + seed) % 3 === 0;
+ if (filled) {
+ rects.push(
+ ``
+ );
+ }
+ }
+ }
+ const size = DEMO_QR_MODULES * DEMO_QR_SCALE;
+ const svg = ``;
+ return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
+}
diff --git a/apps/dashboard/src/utils/query-keys.ts b/apps/dashboard/src/utils/query-keys.ts
index da67ac2be..d628ec363 100644
--- a/apps/dashboard/src/utils/query-keys.ts
+++ b/apps/dashboard/src/utils/query-keys.ts
@@ -2,10 +2,8 @@ export const QUERY_KEYS = {
AUTH: {
session: ["auth", "session"],
organizations: ["auth", "organizations"],
+ security: ["auth", "security"],
activeOrganization: ["auth", "activeOrganization"],
- // Deliberately nested under `activeOrganization` so the existing
- // `invalidateQueries({ queryKey: AUTH.activeOrganization })` call sites
- // reach the per-slug summaries by prefix. Keep the first two segments.
organizationSummary: (slug: string) =>
["auth", "activeOrganization", "summary", slug] as const,
},
diff --git a/apps/dashboard/src/utils/ratelimit.ts b/apps/dashboard/src/utils/ratelimit.ts
index 59684c178..13e80ba98 100644
--- a/apps/dashboard/src/utils/ratelimit.ts
+++ b/apps/dashboard/src/utils/ratelimit.ts
@@ -1,5 +1,6 @@
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
+import { headers } from "next/headers";
import type { NextRequest } from "next/server";
import { COMPANY_LOGO_RATE_LIMIT_PER_QUERY_PER_MINUTE } from "@/constants/company-logo";
@@ -170,6 +171,18 @@ export const ratelimit = {
prefix: "ratelimit:auth-social-start",
limiter: Ratelimit.slidingWindow(10, "1m"),
}),
+ mfaVerify: new Ratelimit({
+ redis,
+ analytics: true,
+ prefix: "ratelimit:auth-mfa-verify",
+ limiter: Ratelimit.slidingWindow(5, "1m"),
+ }),
+ backupCode: new Ratelimit({
+ redis,
+ analytics: true,
+ prefix: "ratelimit:auth-backup-code",
+ limiter: Ratelimit.slidingWindow(5, "10m"),
+ }),
};
export function getClientIpFromHeaders(headersList: Headers): string {
@@ -184,12 +197,42 @@ export function getClientIpFromHeaders(headersList: Headers): string {
}
export function getClientIp(request: NextRequest): string {
- // Vercel injects this header at its trusted network boundary. Do not fall
- // back to generic forwarding headers: outside Vercel they are supplied by
- // the client unless the deployment configures its own trusted proxy.
if (process.env.VERCEL !== "1") {
return "unknown";
}
return request.headers.get("x-vercel-forwarded-for")?.trim() || "unknown";
}
+
+export async function isRateLimited(
+ limiter: Ratelimit,
+ key: string
+): Promise {
+ if (shouldSkipRateLimiting()) {
+ return false;
+ }
+
+ const headersList = await headers();
+ const ip = getClientIpFromHeaders(headersList);
+ const { success } = await limiter.limit(`${ip}:${key.toLowerCase()}`);
+ return !success;
+}
+
+export async function isAccountRateLimited(
+ limiter: Ratelimit,
+ key: string
+): Promise {
+ if (shouldSkipRateLimiting()) {
+ return false;
+ }
+ const { success } = await limiter.limit(key.toLowerCase());
+ return !success;
+}
+
+function shouldSkipRateLimiting() {
+ return (
+ process.env.NODE_ENV !== "production" &&
+ (!process.env.UPSTASH_REDIS_REST_URL ||
+ !process.env.UPSTASH_REDIS_REST_TOKEN)
+ );
+}
diff --git a/bun.lock b/bun.lock
index 16c04d568..f998a97db 100644
--- a/bun.lock
+++ b/bun.lock
@@ -569,6 +569,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@hugeicons/core-free-icons": "^4.1.1",
"@hugeicons/react": "^1.1.4",
+ "@notra/schemas": "workspace:*",
"@shadcn/react": "^0.2.0",
"@shikijs/transformers": "^3.21.0",
"@tailwindcss/typography": "^0.5.19",
diff --git a/packages/db/migrations/0090_user_auth_security.sql b/packages/db/migrations/0090_user_auth_security.sql
new file mode 100644
index 000000000..bb67a8d22
--- /dev/null
+++ b/packages/db/migrations/0090_user_auth_security.sql
@@ -0,0 +1,30 @@
+CREATE TABLE IF NOT EXISTS "user_backup_codes" (
+ "id" text PRIMARY KEY NOT NULL,
+ "user_id" text NOT NULL,
+ "code_hash" text NOT NULL,
+ "used_at" timestamp,
+ "created_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "user_backup_codes" ADD CONSTRAINT "user_backup_codes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "user_backup_codes_userId_idx" ON "user_backup_codes" USING btree ("user_id");--> statement-breakpoint
+CREATE UNIQUE INDEX IF NOT EXISTS "user_backup_codes_userId_codeHash_uidx" ON "user_backup_codes" USING btree ("user_id","code_hash");--> statement-breakpoint
+CREATE TABLE IF NOT EXISTS "user_auth_factor_labels" (
+ "id" text PRIMARY KEY NOT NULL,
+ "user_id" text NOT NULL,
+ "factor_id" text NOT NULL,
+ "name" text NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ CONSTRAINT "user_auth_factor_labels_factor_id_unique" UNIQUE("factor_id")
+);
+--> statement-breakpoint
+DO $$ BEGIN
+ ALTER TABLE "user_auth_factor_labels" ADD CONSTRAINT "user_auth_factor_labels_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
+EXCEPTION
+ WHEN duplicate_object THEN null;
+END $$;--> statement-breakpoint
+CREATE INDEX IF NOT EXISTS "user_auth_factor_labels_userId_idx" ON "user_auth_factor_labels" USING btree ("user_id");
diff --git a/packages/db/migrations/0100_complete_robin_chapel.sql b/packages/db/migrations/0100_complete_robin_chapel.sql
new file mode 100644
index 000000000..1d78017ff
--- /dev/null
+++ b/packages/db/migrations/0100_complete_robin_chapel.sql
@@ -0,0 +1,11 @@
+CREATE TABLE "user_backup_codes" (
+ "id" text PRIMARY KEY NOT NULL,
+ "user_id" text NOT NULL,
+ "code_hash" text NOT NULL,
+ "used_at" timestamp,
+ "created_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "user_backup_codes" ADD CONSTRAINT "user_backup_codes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "user_backup_codes_userId_idx" ON "user_backup_codes" USING btree ("user_id");--> statement-breakpoint
+CREATE UNIQUE INDEX "user_backup_codes_userId_codeHash_uidx" ON "user_backup_codes" USING btree ("user_id","code_hash");
\ No newline at end of file
diff --git a/packages/db/migrations/meta/0100_snapshot.json b/packages/db/migrations/meta/0100_snapshot.json
new file mode 100644
index 000000000..03b770279
--- /dev/null
+++ b/packages/db/migrations/meta/0100_snapshot.json
@@ -0,0 +1,12479 @@
+{
+ "id": "b4bf04a4-bad3-4b39-88cd-8c9708cca859",
+ "prevId": "59e1d159-c5a7-4bfb-a551-e9e7e7314104",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.agent_feedback": {
+ "name": "agent_feedback",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'api'"
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'other'"
+ },
+ "sentiment": {
+ "name": "sentiment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'new'"
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "agent_client": {
+ "name": "agent_client",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "agent_model": {
+ "name": "agent_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tool_version": {
+ "name": "tool_version",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "context_url": {
+ "name": "context_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_id": {
+ "name": "external_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "idempotency_key": {
+ "name": "idempotency_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resolved_at": {
+ "name": "resolved_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": {
+ "agentFeedback_organizationId_createdAt_idx": {
+ "name": "agentFeedback_organizationId_createdAt_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": {}
+ },
+ "agentFeedback_organizationId_status_idx": {
+ "name": "agentFeedback_organizationId_status_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "agentFeedback_projectId_idx": {
+ "name": "agentFeedback_projectId_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "agentFeedback_organizationId_idempotencyKey_uidx": {
+ "name": "agentFeedback_organizationId_idempotencyKey_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "idempotency_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "agent_feedback_organization_id_organizations_id_fk": {
+ "name": "agent_feedback_organization_id_organizations_id_fk",
+ "tableFrom": "agent_feedback",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "agent_feedback_project_id_projects_id_fk": {
+ "name": "agent_feedback_project_id_projects_id_fk",
+ "tableFrom": "agent_feedback",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.agent_sessions": {
+ "name": "agent_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chat_id": {
+ "name": "chat_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "surface": {
+ "name": "surface",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_id": {
+ "name": "content_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "collection_id": {
+ "name": "collection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "eve_session_id": {
+ "name": "eve_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "continuation_token": {
+ "name": "continuation_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stream_index": {
+ "name": "stream_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "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": {
+ "agentSessions_eveSessionId_uidx": {
+ "name": "agentSessions_eveSessionId_uidx",
+ "columns": [
+ {
+ "expression": "eve_session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "agentSessions_organizationId_idx": {
+ "name": "agentSessions_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "agentSessions_chatId_idx": {
+ "name": "agentSessions_chatId_idx",
+ "columns": [
+ {
+ "expression": "chat_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "agent_sessions_organization_id_organizations_id_fk": {
+ "name": "agent_sessions_organization_id_organizations_id_fk",
+ "tableFrom": "agent_sessions",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "agent_sessions_chat_id_chat_sessions_id_fk": {
+ "name": "agent_sessions_chat_id_chat_sessions_id_fk",
+ "tableFrom": "agent_sessions",
+ "tableTo": "chat_sessions",
+ "columnsFrom": [
+ "chat_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.autonomy_actions": {
+ "name": "autonomy_actions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "capability_name": {
+ "name": "capability_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "capability_version": {
+ "name": "capability_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "idempotency_key": {
+ "name": "idempotency_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "autonomy_action_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "external_ref": {
+ "name": "external_ref",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "finished_at": {
+ "name": "finished_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": {
+ "autonomyActions_organizationId_idx": {
+ "name": "autonomyActions_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyActions_runId_idx": {
+ "name": "autonomyActions_runId_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyActions_org_capability_idempotency_uidx": {
+ "name": "autonomyActions_org_capability_idempotency_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "capability_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "idempotency_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyActions_organizationId_status_idx": {
+ "name": "autonomyActions_organizationId_status_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "autonomy_actions_organization_id_organizations_id_fk": {
+ "name": "autonomy_actions_organization_id_organizations_id_fk",
+ "tableFrom": "autonomy_actions",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "autonomy_actions_run_id_autonomy_runs_id_fk": {
+ "name": "autonomy_actions_run_id_autonomy_runs_id_fk",
+ "tableFrom": "autonomy_actions",
+ "tableTo": "autonomy_runs",
+ "columnsFrom": [
+ "run_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "autonomy_actions_task_id_autonomy_tasks_id_fk": {
+ "name": "autonomy_actions_task_id_autonomy_tasks_id_fk",
+ "tableFrom": "autonomy_actions",
+ "tableTo": "autonomy_tasks",
+ "columnsFrom": [
+ "task_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.autonomy_checkpoints": {
+ "name": "autonomy_checkpoints",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state": {
+ "name": "state",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "autonomyCheckpoints_organizationId_idx": {
+ "name": "autonomyCheckpoints_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyCheckpoints_runId_idx": {
+ "name": "autonomyCheckpoints_runId_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "autonomy_checkpoints_organization_id_organizations_id_fk": {
+ "name": "autonomy_checkpoints_organization_id_organizations_id_fk",
+ "tableFrom": "autonomy_checkpoints",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "autonomy_checkpoints_run_id_autonomy_runs_id_fk": {
+ "name": "autonomy_checkpoints_run_id_autonomy_runs_id_fk",
+ "tableFrom": "autonomy_checkpoints",
+ "tableTo": "autonomy_runs",
+ "columnsFrom": [
+ "run_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "autonomy_checkpoints_task_id_autonomy_tasks_id_fk": {
+ "name": "autonomy_checkpoints_task_id_autonomy_tasks_id_fk",
+ "tableFrom": "autonomy_checkpoints",
+ "tableTo": "autonomy_tasks",
+ "columnsFrom": [
+ "task_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.autonomy_claims": {
+ "name": "autonomy_claims",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "claim_key": {
+ "name": "claim_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner_token": {
+ "name": "owner_token",
+ "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": {
+ "autonomyClaims_scope_claimKey_uidx": {
+ "name": "autonomyClaims_scope_claimKey_uidx",
+ "columns": [
+ {
+ "expression": "scope",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "claim_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyClaims_expiresAt_idx": {
+ "name": "autonomyClaims_expiresAt_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "autonomy_claims_organization_id_organizations_id_fk": {
+ "name": "autonomy_claims_organization_id_organizations_id_fk",
+ "tableFrom": "autonomy_claims",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.autonomy_controller_leases": {
+ "name": "autonomy_controller_leases",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner_token": {
+ "name": "owner_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fencing_token": {
+ "name": "fencing_token",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "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": {
+ "autonomyControllerLeases_organizationId_idx": {
+ "name": "autonomyControllerLeases_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "autonomy_controller_leases_organization_id_organizations_id_fk": {
+ "name": "autonomy_controller_leases_organization_id_organizations_id_fk",
+ "tableFrom": "autonomy_controller_leases",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.autonomy_goals": {
+ "name": "autonomy_goals",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mandate_id": {
+ "name": "mandate_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "autonomy_goal_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'open'"
+ },
+ "priority": {
+ "name": "priority",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "origin_signal_ids": {
+ "name": "origin_signal_ids",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "completed_at": {
+ "name": "completed_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": {
+ "autonomyGoals_organizationId_idx": {
+ "name": "autonomyGoals_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyGoals_mandateId_idx": {
+ "name": "autonomyGoals_mandateId_idx",
+ "columns": [
+ {
+ "expression": "mandate_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyGoals_organizationId_status_idx": {
+ "name": "autonomyGoals_organizationId_status_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "autonomy_goals_organization_id_organizations_id_fk": {
+ "name": "autonomy_goals_organization_id_organizations_id_fk",
+ "tableFrom": "autonomy_goals",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "autonomy_goals_mandate_id_autonomy_mandates_id_fk": {
+ "name": "autonomy_goals_mandate_id_autonomy_mandates_id_fk",
+ "tableFrom": "autonomy_goals",
+ "tableTo": "autonomy_mandates",
+ "columnsFrom": [
+ "mandate_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.autonomy_mandates": {
+ "name": "autonomy_mandates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "objective": {
+ "name": "objective",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "policy": {
+ "name": "policy",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "autonomy_mandate_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "qstash_schedule_id": {
+ "name": "qstash_schedule_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "paused_at": {
+ "name": "paused_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": {
+ "autonomyMandates_organizationId_idx": {
+ "name": "autonomyMandates_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyMandates_organizationId_name_uidx": {
+ "name": "autonomyMandates_organizationId_name_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "autonomy_mandates_organization_id_organizations_id_fk": {
+ "name": "autonomy_mandates_organization_id_organizations_id_fk",
+ "tableFrom": "autonomy_mandates",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "autonomy_mandates_created_by_user_id_users_id_fk": {
+ "name": "autonomy_mandates_created_by_user_id_users_id_fk",
+ "tableFrom": "autonomy_mandates",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.autonomy_outbox": {
+ "name": "autonomy_outbox",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "destination": {
+ "name": "destination",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dedupe_key": {
+ "name": "dedupe_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "autonomy_outbox_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivered_at": {
+ "name": "delivered_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": {
+ "autonomyOutbox_organizationId_idx": {
+ "name": "autonomyOutbox_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyOutbox_org_destination_dedupeKey_uidx": {
+ "name": "autonomyOutbox_org_destination_dedupeKey_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "destination",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "dedupe_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyOutbox_status_nextAttemptAt_idx": {
+ "name": "autonomyOutbox_status_nextAttemptAt_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "autonomy_outbox_organization_id_organizations_id_fk": {
+ "name": "autonomy_outbox_organization_id_organizations_id_fk",
+ "tableFrom": "autonomy_outbox",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "autonomy_outbox_run_id_autonomy_runs_id_fk": {
+ "name": "autonomy_outbox_run_id_autonomy_runs_id_fk",
+ "tableFrom": "autonomy_outbox",
+ "tableTo": "autonomy_runs",
+ "columnsFrom": [
+ "run_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.autonomy_runs": {
+ "name": "autonomy_runs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mandate_id": {
+ "name": "mandate_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mandate_version": {
+ "name": "mandate_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "goal_id": {
+ "name": "goal_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "trigger": {
+ "name": "trigger",
+ "type": "autonomy_run_trigger",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "planner_input_hash": {
+ "name": "planner_input_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "planner_output": {
+ "name": "planner_output",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "autonomy_run_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'planning'"
+ },
+ "cost_cents": {
+ "name": "cost_cents",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_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": {
+ "autonomyRuns_organizationId_idx": {
+ "name": "autonomyRuns_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyRuns_mandateId_idx": {
+ "name": "autonomyRuns_mandateId_idx",
+ "columns": [
+ {
+ "expression": "mandate_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyRuns_goalId_idx": {
+ "name": "autonomyRuns_goalId_idx",
+ "columns": [
+ {
+ "expression": "goal_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyRuns_organizationId_status_idx": {
+ "name": "autonomyRuns_organizationId_status_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "autonomy_runs_organization_id_organizations_id_fk": {
+ "name": "autonomy_runs_organization_id_organizations_id_fk",
+ "tableFrom": "autonomy_runs",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "autonomy_runs_mandate_id_autonomy_mandates_id_fk": {
+ "name": "autonomy_runs_mandate_id_autonomy_mandates_id_fk",
+ "tableFrom": "autonomy_runs",
+ "tableTo": "autonomy_mandates",
+ "columnsFrom": [
+ "mandate_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "autonomy_runs_goal_id_autonomy_goals_id_fk": {
+ "name": "autonomy_runs_goal_id_autonomy_goals_id_fk",
+ "tableFrom": "autonomy_runs",
+ "tableTo": "autonomy_goals",
+ "columnsFrom": [
+ "goal_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.autonomy_signals": {
+ "name": "autonomy_signals",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_event_id": {
+ "name": "source_event_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dedupe_hash": {
+ "name": "dedupe_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "autonomy_signal_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "coalesced_into_signal_id": {
+ "name": "coalesced_into_signal_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "occurred_at": {
+ "name": "occurred_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "processed_at": {
+ "name": "processed_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": {
+ "autonomySignals_organizationId_idx": {
+ "name": "autonomySignals_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomySignals_organizationId_dedupeHash_uidx": {
+ "name": "autonomySignals_organizationId_dedupeHash_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "dedupe_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomySignals_organizationId_status_occurredAt_idx": {
+ "name": "autonomySignals_organizationId_status_occurredAt_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "occurred_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "autonomy_signals_organization_id_organizations_id_fk": {
+ "name": "autonomy_signals_organization_id_organizations_id_fk",
+ "tableFrom": "autonomy_signals",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "autonomySignals_coalescedIntoSignalId_fk": {
+ "name": "autonomySignals_coalescedIntoSignalId_fk",
+ "tableFrom": "autonomy_signals",
+ "tableTo": "autonomy_signals",
+ "columnsFrom": [
+ "coalesced_into_signal_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.autonomy_tasks": {
+ "name": "autonomy_tasks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "goal_id": {
+ "name": "goal_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "capability_name": {
+ "name": "capability_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "capability_version": {
+ "name": "capability_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "params": {
+ "name": "params",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "depends_on_task_ids": {
+ "name": "depends_on_task_ids",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "status": {
+ "name": "status",
+ "type": "autonomy_task_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempt": {
+ "name": "attempt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "wait_until": {
+ "name": "wait_until",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "result": {
+ "name": "result",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_at": {
+ "name": "completed_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": {
+ "autonomyTasks_organizationId_idx": {
+ "name": "autonomyTasks_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyTasks_goalId_idx": {
+ "name": "autonomyTasks_goalId_idx",
+ "columns": [
+ {
+ "expression": "goal_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyTasks_runId_idx": {
+ "name": "autonomyTasks_runId_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "autonomyTasks_organizationId_status_waitUntil_idx": {
+ "name": "autonomyTasks_organizationId_status_waitUntil_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "wait_until",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "autonomy_tasks_organization_id_organizations_id_fk": {
+ "name": "autonomy_tasks_organization_id_organizations_id_fk",
+ "tableFrom": "autonomy_tasks",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "autonomy_tasks_goal_id_autonomy_goals_id_fk": {
+ "name": "autonomy_tasks_goal_id_autonomy_goals_id_fk",
+ "tableFrom": "autonomy_tasks",
+ "tableTo": "autonomy_goals",
+ "columnsFrom": [
+ "goal_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "autonomy_tasks_run_id_autonomy_runs_id_fk": {
+ "name": "autonomy_tasks_run_id_autonomy_runs_id_fk",
+ "tableFrom": "autonomy_tasks",
+ "tableTo": "autonomy_runs",
+ "columnsFrom": [
+ "run_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.brand_guideline_assets": {
+ "name": "brand_guideline_assets",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "guideline_id": {
+ "name": "guideline_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "brand_guideline_asset_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_key": {
+ "name": "storage_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "format": {
+ "name": "format",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mime_type": {
+ "name": "mime_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "width": {
+ "name": "width",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "height": {
+ "name": "height",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "aspect_ratio": {
+ "name": "aspect_ratio",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "variant": {
+ "name": "variant",
+ "type": "brand_guideline_asset_variant",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "captured_at": {
+ "name": "captured_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "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": {
+ "brandGuidelineAssets_guidelineId_idx": {
+ "name": "brandGuidelineAssets_guidelineId_idx",
+ "columns": [
+ {
+ "expression": "guideline_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandGuidelineAssets_guideline_kind_idx": {
+ "name": "brandGuidelineAssets_guideline_kind_idx",
+ "columns": [
+ {
+ "expression": "guideline_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandGuidelineAssets_guideline_kind_variant_uidx": {
+ "name": "brandGuidelineAssets_guideline_kind_variant_uidx",
+ "columns": [
+ {
+ "expression": "guideline_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "variant",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "brand_guideline_assets_guideline_id_brand_guidelines_id_fk": {
+ "name": "brand_guideline_assets_guideline_id_brand_guidelines_id_fk",
+ "tableFrom": "brand_guideline_assets",
+ "tableTo": "brand_guidelines",
+ "columnsFrom": [
+ "guideline_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.brand_guideline_colors": {
+ "name": "brand_guideline_colors",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "guideline_id": {
+ "name": "guideline_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "brand_guideline_color_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'custom'"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "light_value": {
+ "name": "light_value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dark_value": {
+ "name": "dark_value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "usage": {
+ "name": "usage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "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": {
+ "brandGuidelineColors_guidelineId_idx": {
+ "name": "brandGuidelineColors_guidelineId_idx",
+ "columns": [
+ {
+ "expression": "guideline_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandGuidelineColors_guideline_role_idx": {
+ "name": "brandGuidelineColors_guideline_role_idx",
+ "columns": [
+ {
+ "expression": "guideline_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "role",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "brand_guideline_colors_guideline_id_brand_guidelines_id_fk": {
+ "name": "brand_guideline_colors_guideline_id_brand_guidelines_id_fk",
+ "tableFrom": "brand_guideline_colors",
+ "tableTo": "brand_guidelines",
+ "columnsFrom": [
+ "guideline_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.brand_guideline_fonts": {
+ "name": "brand_guideline_fonts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "guideline_id": {
+ "name": "guideline_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "brand_guideline_font_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'unknown'"
+ },
+ "family": {
+ "name": "family",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "weight": {
+ "name": "weight",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "size": {
+ "name": "size",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "line_height": {
+ "name": "line_height",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "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": {
+ "brandGuidelineFonts_guidelineId_idx": {
+ "name": "brandGuidelineFonts_guidelineId_idx",
+ "columns": [
+ {
+ "expression": "guideline_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandGuidelineFonts_guideline_role_idx": {
+ "name": "brandGuidelineFonts_guideline_role_idx",
+ "columns": [
+ {
+ "expression": "guideline_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "role",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "brand_guideline_fonts_guideline_id_brand_guidelines_id_fk": {
+ "name": "brand_guideline_fonts_guideline_id_brand_guidelines_id_fk",
+ "tableFrom": "brand_guideline_fonts",
+ "tableTo": "brand_guidelines",
+ "columnsFrom": [
+ "guideline_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.brand_guideline_screenshots": {
+ "name": "brand_guideline_screenshots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "guideline_id": {
+ "name": "guideline_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "brand_guideline_screenshot_kind",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_key": {
+ "name": "storage_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "width": {
+ "name": "width",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "height": {
+ "name": "height",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "format": {
+ "name": "format",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "full_page": {
+ "name": "full_page",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "captured_at": {
+ "name": "captured_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "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": {
+ "brandGuidelineScreenshots_guidelineId_idx": {
+ "name": "brandGuidelineScreenshots_guidelineId_idx",
+ "columns": [
+ {
+ "expression": "guideline_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandGuidelineScreenshots_guideline_kind_uidx": {
+ "name": "brandGuidelineScreenshots_guideline_kind_uidx",
+ "columns": [
+ {
+ "expression": "guideline_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "brand_guideline_screenshots_guideline_id_brand_guidelines_id_fk": {
+ "name": "brand_guideline_screenshots_guideline_id_brand_guidelines_id_fk",
+ "tableFrom": "brand_guideline_screenshots",
+ "tableTo": "brand_guidelines",
+ "columnsFrom": [
+ "guideline_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.brand_guideline_tokens": {
+ "name": "brand_guideline_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "guideline_id": {
+ "name": "guideline_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "brand_guideline_token_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'unknown'"
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "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": {
+ "brandGuidelineTokens_guidelineId_idx": {
+ "name": "brandGuidelineTokens_guidelineId_idx",
+ "columns": [
+ {
+ "expression": "guideline_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandGuidelineTokens_guideline_type_idx": {
+ "name": "brandGuidelineTokens_guideline_type_idx",
+ "columns": [
+ {
+ "expression": "guideline_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "brand_guideline_tokens_guideline_id_brand_guidelines_id_fk": {
+ "name": "brand_guideline_tokens_guideline_id_brand_guidelines_id_fk",
+ "tableFrom": "brand_guideline_tokens",
+ "tableTo": "brand_guidelines",
+ "columnsFrom": [
+ "guideline_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.brand_guidelines": {
+ "name": "brand_guidelines",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "brand_settings_id": {
+ "name": "brand_settings_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "brand_guideline_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "context_dev_meta": {
+ "name": "context_dev_meta",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_generated_at": {
+ "name": "last_generated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_generation_error": {
+ "name": "last_generation_error",
+ "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": {
+ "brandGuidelines_brandSettingsId_uidx": {
+ "name": "brandGuidelines_brandSettingsId_uidx",
+ "columns": [
+ {
+ "expression": "brand_settings_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandGuidelines_status_idx": {
+ "name": "brandGuidelines_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "brand_guidelines_brand_settings_id_brand_settings_id_fk": {
+ "name": "brand_guidelines_brand_settings_id_brand_settings_id_fk",
+ "tableFrom": "brand_guidelines",
+ "tableTo": "brand_settings",
+ "columnsFrom": [
+ "brand_settings_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.brand_references": {
+ "name": "brand_references",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "brand_settings_id": {
+ "name": "brand_settings_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "reference_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_url": {
+ "name": "source_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_snapshot_key": {
+ "name": "source_snapshot_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_content_hash": {
+ "name": "source_content_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source_captured_at": {
+ "name": "source_captured_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "supermemory_document_id": {
+ "name": "supermemory_document_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "supermemory_memory_id": {
+ "name": "supermemory_memory_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "supermemory_synced_at": {
+ "name": "supermemory_synced_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "supermemory_last_sync_error": {
+ "name": "supermemory_last_sync_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applicable_to": {
+ "name": "applicable_to",
+ "type": "applicable_platform[]",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY['all']::applicable_platform[]"
+ },
+ "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": {
+ "brandReferences_brandSettingsId_idx": {
+ "name": "brandReferences_brandSettingsId_idx",
+ "columns": [
+ {
+ "expression": "brand_settings_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandReferences_brandSettingsId_sourceUrl_idx": {
+ "name": "brandReferences_brandSettingsId_sourceUrl_idx",
+ "columns": [
+ {
+ "expression": "brand_settings_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "brand_references_brand_settings_id_brand_settings_id_fk": {
+ "name": "brand_references_brand_settings_id_brand_settings_id_fk",
+ "tableFrom": "brand_references",
+ "tableTo": "brand_settings",
+ "columnsFrom": [
+ "brand_settings_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.brand_settings": {
+ "name": "brand_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Default'"
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "website_url": {
+ "name": "website_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_name": {
+ "name": "company_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "company_description": {
+ "name": "company_description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tone_profile": {
+ "name": "tone_profile",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "custom_tone": {
+ "name": "custom_tone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "custom_instructions": {
+ "name": "custom_instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "audience": {
+ "name": "audience",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "language": {
+ "name": "language",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'English'"
+ },
+ "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": {
+ "brandSettings_org_name_uidx": {
+ "name": "brandSettings_org_name_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandSettings_org_default_uidx": {
+ "name": "brandSettings_org_default_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"brand_settings\".\"is_default\" = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandSettings_organizationId_idx": {
+ "name": "brandSettings_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "brand_settings_organization_id_organizations_id_fk": {
+ "name": "brand_settings_organization_id_organizations_id_fk",
+ "tableFrom": "brand_settings",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "brandSettings_toneProfile_check": {
+ "name": "brandSettings_toneProfile_check",
+ "value": "\"brand_settings\".\"tone_profile\" IS NULL OR \"brand_settings\".\"tone_profile\" IN ('Conversational', 'Professional', 'Casual', 'Formal')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.brand_sitemap_pages": {
+ "name": "brand_sitemap_pages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "sitemap_id": {
+ "name": "sitemap_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "category": {
+ "name": "category",
+ "type": "brand_sitemap_page_category",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status_code": {
+ "name": "status_code",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "redirect_target": {
+ "name": "redirect_target",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "word_count": {
+ "name": "word_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "text_ratio": {
+ "name": "text_ratio",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "internal_links": {
+ "name": "internal_links",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_links": {
+ "name": "external_links",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "crawled_at": {
+ "name": "crawled_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": {
+ "brandSitemapPages_sitemapId_idx": {
+ "name": "brandSitemapPages_sitemapId_idx",
+ "columns": [
+ {
+ "expression": "sitemap_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandSitemapPages_sitemap_category_idx": {
+ "name": "brandSitemapPages_sitemap_category_idx",
+ "columns": [
+ {
+ "expression": "sitemap_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "category",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandSitemapPages_sitemap_category_wordCount_idx": {
+ "name": "brandSitemapPages_sitemap_category_wordCount_idx",
+ "columns": [
+ {
+ "expression": "sitemap_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "category",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "word_count",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandSitemapPages_sitemap_url_uidx": {
+ "name": "brandSitemapPages_sitemap_url_uidx",
+ "columns": [
+ {
+ "expression": "sitemap_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "brand_sitemap_pages_sitemap_id_brand_sitemaps_id_fk": {
+ "name": "brand_sitemap_pages_sitemap_id_brand_sitemaps_id_fk",
+ "tableFrom": "brand_sitemap_pages",
+ "tableTo": "brand_sitemaps",
+ "columnsFrom": [
+ "sitemap_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.brand_sitemaps": {
+ "name": "brand_sitemaps",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "brand_settings_id": {
+ "name": "brand_settings_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "label": {
+ "name": "label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hostname": {
+ "name": "hostname",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "brand_sitemap_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "total_pages": {
+ "name": "total_pages",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "indexed_pages": {
+ "name": "indexed_pages",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "failed_pages": {
+ "name": "failed_pages",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "context_dev_meta": {
+ "name": "context_dev_meta",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_crawl_started_at": {
+ "name": "last_crawl_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_crawled_at": {
+ "name": "last_crawled_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_crawl_error": {
+ "name": "last_crawl_error",
+ "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": {
+ "brandSitemaps_brandSettingsId_idx": {
+ "name": "brandSitemaps_brandSettingsId_idx",
+ "columns": [
+ {
+ "expression": "brand_settings_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "brandSitemaps_brandSettings_url_uidx": {
+ "name": "brandSitemaps_brandSettings_url_uidx",
+ "columns": [
+ {
+ "expression": "brand_settings_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "brand_sitemaps_brand_settings_id_brand_settings_id_fk": {
+ "name": "brand_sitemaps_brand_settings_id_brand_settings_id_fk",
+ "tableFrom": "brand_sitemaps",
+ "tableTo": "brand_settings",
+ "columnsFrom": [
+ "brand_settings_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.chat_attachments": {
+ "name": "chat_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filename": {
+ "name": "filename",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "media_type": {
+ "name": "media_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size": {
+ "name": "size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "chatAttachments_organizationId_createdAt_idx": {
+ "name": "chatAttachments_organizationId_createdAt_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": {}
+ },
+ "chatAttachments_userId_idx": {
+ "name": "chatAttachments_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "chat_attachments_organization_id_organizations_id_fk": {
+ "name": "chat_attachments_organization_id_organizations_id_fk",
+ "tableFrom": "chat_attachments",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_attachments_user_id_users_id_fk": {
+ "name": "chat_attachments_user_id_users_id_fk",
+ "tableFrom": "chat_attachments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "chat_attachments_key_unique": {
+ "name": "chat_attachments_key_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "key"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.chat_sessions": {
+ "name": "chat_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_id": {
+ "name": "content_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "messages": {
+ "name": "messages",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "pinned_at": {
+ "name": "pinned_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_channel_source": {
+ "name": "external_channel_source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "external_channel_id": {
+ "name": "external_channel_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": {
+ "chatSessions_organizationId_idx": {
+ "name": "chatSessions_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "chatSessions_organizationId_deletedAt_idx": {
+ "name": "chatSessions_organizationId_deletedAt_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "chatSessions_org_project_idx": {
+ "name": "chatSessions_org_project_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "chatSessions_org_content_deleted_updated_idx": {
+ "name": "chatSessions_org_content_deleted_updated_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "content_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "deleted_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "chatSessions_org_externalChannel_uidx": {
+ "name": "chatSessions_org_externalChannel_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_channel_source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "external_channel_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"chat_sessions\".\"external_channel_source\" IN ('discord', 'slack') AND \"chat_sessions\".\"external_channel_id\" IS NOT NULL AND \"chat_sessions\".\"deleted_at\" IS NULL",
+ "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",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_sessions_content_id_posts_id_fk": {
+ "name": "chat_sessions_content_id_posts_id_fk",
+ "tableFrom": "chat_sessions",
+ "tableTo": "posts",
+ "columnsFrom": [
+ "content_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_sessions_project_id_projects_id_fk": {
+ "name": "chat_sessions_project_id_projects_id_fk",
+ "tableFrom": "chat_sessions",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.connected_social_accounts": {
+ "name": "connected_social_accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_account_id": {
+ "name": "provider_account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile_image_url": {
+ "name": "profile_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "verified": {
+ "name": "verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "verified_type": {
+ "name": "verified_type",
+ "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": {
+ "connectedSocialAccounts_organizationId_idx": {
+ "name": "connectedSocialAccounts_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "connectedSocialAccounts_org_provider_account_uidx": {
+ "name": "connectedSocialAccounts_org_provider_account_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "connected_social_accounts_organization_id_organizations_id_fk": {
+ "name": "connected_social_accounts_organization_id_organizations_id_fk",
+ "tableFrom": "connected_social_accounts",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.content_publications": {
+ "name": "content_publications",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "post_id": {
+ "name": "post_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner": {
+ "name": "owner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repo": {
+ "name": "repo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "branch": {
+ "name": "branch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pull_request_number": {
+ "name": "pull_request_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pull_request_url": {
+ "name": "pull_request_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "head_sha": {
+ "name": "head_sha",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'open'"
+ },
+ "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": {
+ "contentPublications_organizationId_idx": {
+ "name": "contentPublications_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "contentPublications_postId_idx": {
+ "name": "contentPublications_postId_idx",
+ "columns": [
+ {
+ "expression": "post_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "contentPublications_repository_pullRequest_uidx": {
+ "name": "contentPublications_repository_pullRequest_uidx",
+ "columns": [
+ {
+ "expression": "repository_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pull_request_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "contentPublications_open_post_uidx": {
+ "name": "contentPublications_open_post_uidx",
+ "columns": [
+ {
+ "expression": "post_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"content_publications\".\"status\" = 'open'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "contentPublications_org_owner_repo_pr_idx": {
+ "name": "contentPublications_org_owner_repo_pr_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "owner",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repo",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "pull_request_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "content_publications_organization_id_organizations_id_fk": {
+ "name": "content_publications_organization_id_organizations_id_fk",
+ "tableFrom": "content_publications",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "content_publications_post_id_posts_id_fk": {
+ "name": "content_publications_post_id_posts_id_fk",
+ "tableFrom": "content_publications",
+ "tableTo": "posts",
+ "columnsFrom": [
+ "post_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "content_publications_repository_id_github_integrations_id_fk": {
+ "name": "content_publications_repository_id_github_integrations_id_fk",
+ "tableFrom": "content_publications",
+ "tableTo": "github_integrations",
+ "columnsFrom": [
+ "repository_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.content_trigger_lookback_windows": {
+ "name": "content_trigger_lookback_windows",
+ "schema": "",
+ "columns": {
+ "trigger_id": {
+ "name": "trigger_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "window": {
+ "name": "window",
+ "type": "lookback_window",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "content_trigger_lookback_windows_trigger_id_content_triggers_id_fk": {
+ "name": "content_trigger_lookback_windows_trigger_id_content_triggers_id_fk",
+ "tableFrom": "content_trigger_lookback_windows",
+ "tableTo": "content_triggers",
+ "columnsFrom": [
+ "trigger_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.content_triggers": {
+ "name": "content_triggers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'Untitled Schedule'"
+ },
+ "source_type": {
+ "name": "source_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_config": {
+ "name": "source_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "targets": {
+ "name": "targets",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "output_type": {
+ "name": "output_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "output_config": {
+ "name": "output_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dedupe_hash": {
+ "name": "dedupe_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "qstash_schedule_id": {
+ "name": "qstash_schedule_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auto_publish": {
+ "name": "auto_publish",
+ "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": {
+ "contentTriggers_organizationId_idx": {
+ "name": "contentTriggers_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "contentTriggers_organization_dedupe_uidx": {
+ "name": "contentTriggers_organization_dedupe_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "dedupe_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "content_triggers_organization_id_organizations_id_fk": {
+ "name": "content_triggers_organization_id_organizations_id_fk",
+ "tableFrom": "content_triggers",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.discussion_comments": {
+ "name": "discussion_comments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "feedback_id": {
+ "name": "feedback_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "shelf_source_id": {
+ "name": "shelf_source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "depth": {
+ "name": "depth",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "edited_at": {
+ "name": "edited_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "discussionComments_feedback_idx": {
+ "name": "discussionComments_feedback_idx",
+ "columns": [
+ {
+ "expression": "feedback_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "discussionComments_shelf_idx": {
+ "name": "discussionComments_shelf_idx",
+ "columns": [
+ {
+ "expression": "shelf_source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "discussion_comments_organization_id_organizations_id_fk": {
+ "name": "discussion_comments_organization_id_organizations_id_fk",
+ "tableFrom": "discussion_comments",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "discussion_comments_feedback_id_agent_feedback_id_fk": {
+ "name": "discussion_comments_feedback_id_agent_feedback_id_fk",
+ "tableFrom": "discussion_comments",
+ "tableTo": "agent_feedback",
+ "columnsFrom": [
+ "feedback_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "discussion_comments_shelf_source_id_geo_shelf_sources_id_fk": {
+ "name": "discussion_comments_shelf_source_id_geo_shelf_sources_id_fk",
+ "tableFrom": "discussion_comments",
+ "tableTo": "geo_shelf_sources",
+ "columnsFrom": [
+ "shelf_source_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "discussion_comments_user_id_users_id_fk": {
+ "name": "discussion_comments_user_id_users_id_fk",
+ "tableFrom": "discussion_comments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "discussion_comments_parent_id_discussion_comments_id_fk": {
+ "name": "discussion_comments_parent_id_discussion_comments_id_fk",
+ "tableFrom": "discussion_comments",
+ "tableTo": "discussion_comments",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "discussionComments_target_check": {
+ "name": "discussionComments_target_check",
+ "value": "num_nonnulls(\"discussion_comments\".\"feedback_id\", \"discussion_comments\".\"shelf_source_id\") = 1"
+ },
+ "discussionComments_depth_check": {
+ "name": "discussionComments_depth_check",
+ "value": "\"discussion_comments\".\"depth\" between 0 and 5"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.discussion_reactions": {
+ "name": "discussion_reactions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "emoji": {
+ "name": "emoji",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "discussionReactions_unique": {
+ "name": "discussionReactions_unique",
+ "columns": [
+ {
+ "expression": "comment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "emoji",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "discussion_reactions_comment_id_discussion_comments_id_fk": {
+ "name": "discussion_reactions_comment_id_discussion_comments_id_fk",
+ "tableFrom": "discussion_reactions",
+ "tableTo": "discussion_comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "discussion_reactions_user_id_users_id_fk": {
+ "name": "discussion_reactions_user_id_users_id_fk",
+ "tableFrom": "discussion_reactions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_agent_readiness_reports": {
+ "name": "geo_agent_readiness_reports",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_url": {
+ "name": "target_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "score": {
+ "name": "score",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "score_label": {
+ "name": "score_label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "score_breakdown": {
+ "name": "score_breakdown",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'null'::jsonb"
+ },
+ "issues": {
+ "name": "issues",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "eligible_checks": {
+ "name": "eligible_checks",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "report_url": {
+ "name": "report_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scanned_at": {
+ "name": "scanned_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": {
+ "geoAgentReadinessReports_organizationId_idx": {
+ "name": "geoAgentReadinessReports_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoAgentReadinessReports_projectId_createdAt_idx": {
+ "name": "geoAgentReadinessReports_projectId_createdAt_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoAgentReadinessReports_projectId_running_uidx": {
+ "name": "geoAgentReadinessReports_projectId_running_uidx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"geo_agent_readiness_reports\".\"status\" = 'running'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_agent_readiness_reports_organization_id_organizations_id_fk": {
+ "name": "geo_agent_readiness_reports_organization_id_organizations_id_fk",
+ "tableFrom": "geo_agent_readiness_reports",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_agent_readiness_reports_project_id_projects_id_fk": {
+ "name": "geo_agent_readiness_reports_project_id_projects_id_fk",
+ "tableFrom": "geo_agent_readiness_reports",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_competitors": {
+ "name": "geo_competitors",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "synonyms": {
+ "name": "synonyms",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'direct'"
+ },
+ "color": {
+ "name": "color",
+ "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": {
+ "geoCompetitors_organizationId_idx": {
+ "name": "geoCompetitors_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoCompetitors_projectId_idx": {
+ "name": "geoCompetitors_projectId_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoCompetitors_projectId_name_uidx": {
+ "name": "geoCompetitors_projectId_name_uidx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_competitors_organization_id_organizations_id_fk": {
+ "name": "geo_competitors_organization_id_organizations_id_fk",
+ "tableFrom": "geo_competitors",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_competitors_project_id_projects_id_fk": {
+ "name": "geo_competitors_project_id_projects_id_fk",
+ "tableFrom": "geo_competitors",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_content_briefs": {
+ "name": "geo_content_briefs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "brand_settings_id": {
+ "name": "brand_settings_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "topic": {
+ "name": "topic",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "brief": {
+ "name": "brief",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "auto_approved": {
+ "name": "auto_approved",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "collection_id": {
+ "name": "collection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "post_id": {
+ "name": "post_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "humanized": {
+ "name": "humanized",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "source_kind": {
+ "name": "source_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'manual'"
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error": {
+ "name": "error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "approved_at": {
+ "name": "approved_at",
+ "type": "timestamp",
+ "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
+ },
+ "published_at": {
+ "name": "published_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rescan_scan_id": {
+ "name": "rescan_scan_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rescan_requested_at": {
+ "name": "rescan_requested_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": {
+ "geoContentBriefs_organizationId_createdAt_idx": {
+ "name": "geoContentBriefs_organizationId_createdAt_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": {}
+ },
+ "geoContentBriefs_projectId_status_idx": {
+ "name": "geoContentBriefs_projectId_status_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoContentBriefs_project_source_updated_idx": {
+ "name": "geoContentBriefs_project_source_updated_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoContentBriefs_open_source_uidx": {
+ "name": "geoContentBriefs_open_source_uidx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"geo_content_briefs\".\"source_kind\" <> 'manual' AND \"geo_content_briefs\".\"source_id\" IS NOT NULL AND \"geo_content_briefs\".\"status\" IN ('draft', 'approved', 'writing', 'failed')",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_content_briefs_organization_id_organizations_id_fk": {
+ "name": "geo_content_briefs_organization_id_organizations_id_fk",
+ "tableFrom": "geo_content_briefs",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_content_briefs_project_id_projects_id_fk": {
+ "name": "geo_content_briefs_project_id_projects_id_fk",
+ "tableFrom": "geo_content_briefs",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_content_briefs_brand_settings_id_brand_settings_id_fk": {
+ "name": "geo_content_briefs_brand_settings_id_brand_settings_id_fk",
+ "tableFrom": "geo_content_briefs",
+ "tableTo": "brand_settings",
+ "columnsFrom": [
+ "brand_settings_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ },
+ "geo_content_briefs_created_by_user_id_users_id_fk": {
+ "name": "geo_content_briefs_created_by_user_id_users_id_fk",
+ "tableFrom": "geo_content_briefs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "geo_content_briefs_collection_id_post_collections_id_fk": {
+ "name": "geo_content_briefs_collection_id_post_collections_id_fk",
+ "tableFrom": "geo_content_briefs",
+ "tableTo": "post_collections",
+ "columnsFrom": [
+ "collection_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "geo_content_briefs_post_id_posts_id_fk": {
+ "name": "geo_content_briefs_post_id_posts_id_fk",
+ "tableFrom": "geo_content_briefs",
+ "tableTo": "posts",
+ "columnsFrom": [
+ "post_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_mention_checks": {
+ "name": "geo_mention_checks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scan_id": {
+ "name": "scan_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "engine": {
+ "name": "engine",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prompt_id": {
+ "name": "prompt_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sequence_id": {
+ "name": "sequence_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "persona_id": {
+ "name": "persona_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "persona_snapshot": {
+ "name": "persona_snapshot",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "turn": {
+ "name": "turn",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "answer": {
+ "name": "answer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mentioned": {
+ "name": "mentioned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owned_source_cited": {
+ "name": "owned_source_cited",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sentiment": {
+ "name": "sentiment",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "competitors": {
+ "name": "competitors",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "excerpt": {
+ "name": "excerpt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "grounding": {
+ "name": "grounding",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"queries\":[],\"sources\":[]}'::jsonb"
+ },
+ "language": {
+ "name": "language",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'English'"
+ },
+ "sources": {
+ "name": "sources",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "finish_reason": {
+ "name": "finish_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "prompt_tokens": {
+ "name": "prompt_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "output_tokens": {
+ "name": "output_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reasoning_tokens": {
+ "name": "reasoning_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "zdr_enforced": {
+ "name": "zdr_enforced",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cost_usd": {
+ "name": "cost_usd",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "judge_tokens": {
+ "name": "judge_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "captured_at": {
+ "name": "captured_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "geoMentionChecks_organizationId_capturedAt_idx": {
+ "name": "geoMentionChecks_organizationId_capturedAt_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "captured_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoMentionChecks_projectId_capturedAt_idx": {
+ "name": "geoMentionChecks_projectId_capturedAt_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "captured_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoMentionChecks_projectEnginePrompt_idx": {
+ "name": "geoMentionChecks_projectEnginePrompt_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "engine",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "prompt_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "captured_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoMentionChecks_project_captured_cover_idx": {
+ "name": "geoMentionChecks_project_captured_cover_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "captured_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "language",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "engine",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "prompt_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "mentioned",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "owned_source_cited",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "position",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sentiment",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sequence_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoMentionChecks_project_prompt_engine_captured_idx": {
+ "name": "geoMentionChecks_project_prompt_engine_captured_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "prompt_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "engine",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "captured_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoMentionChecks_competitors_idx": {
+ "name": "geoMentionChecks_competitors_idx",
+ "columns": [
+ {
+ "expression": "competitors",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "geoMentionChecks_sequence_turn_engine_captured_idx": {
+ "name": "geoMentionChecks_sequence_turn_engine_captured_idx",
+ "columns": [
+ {
+ "expression": "sequence_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "turn",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "engine",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "captured_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"geo_mention_checks\".\"sequence_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoMentionChecks_scanId_idx": {
+ "name": "geoMentionChecks_scanId_idx",
+ "columns": [
+ {
+ "expression": "scan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoMentionChecks_personaId_capturedAt_idx": {
+ "name": "geoMentionChecks_personaId_capturedAt_idx",
+ "columns": [
+ {
+ "expression": "persona_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "captured_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoMentionChecks_scanEnginePromptTurnLanguage_uidx": {
+ "name": "geoMentionChecks_scanEnginePromptTurnLanguage_uidx",
+ "columns": [
+ {
+ "expression": "scan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "engine",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "prompt_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "turn",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "language",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_mention_checks_organization_id_organizations_id_fk": {
+ "name": "geo_mention_checks_organization_id_organizations_id_fk",
+ "tableFrom": "geo_mention_checks",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_mention_checks_project_id_projects_id_fk": {
+ "name": "geo_mention_checks_project_id_projects_id_fk",
+ "tableFrom": "geo_mention_checks",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_mention_checks_scan_id_geo_scans_id_fk": {
+ "name": "geo_mention_checks_scan_id_geo_scans_id_fk",
+ "tableFrom": "geo_mention_checks",
+ "tableTo": "geo_scans",
+ "columnsFrom": [
+ "scan_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_mention_checks_persona_id_geo_personas_id_fk": {
+ "name": "geo_mention_checks_persona_id_geo_personas_id_fk",
+ "tableFrom": "geo_mention_checks",
+ "tableTo": "geo_personas",
+ "columnsFrom": [
+ "persona_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "geoMentionChecks_personaSnapshot_check": {
+ "name": "geoMentionChecks_personaSnapshot_check",
+ "value": "\"geo_mention_checks\".\"persona_id\" IS NULL OR \"geo_mention_checks\".\"persona_snapshot\" IS NOT NULL"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.geo_persona_memories": {
+ "name": "geo_persona_memories",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "persona_id": {
+ "name": "persona_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "geoPersonaMemories_personaId_idx": {
+ "name": "geoPersonaMemories_personaId_idx",
+ "columns": [
+ {
+ "expression": "persona_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoPersonaMemories_projectId_idx": {
+ "name": "geoPersonaMemories_projectId_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_persona_memories_persona_id_geo_personas_id_fk": {
+ "name": "geo_persona_memories_persona_id_geo_personas_id_fk",
+ "tableFrom": "geo_persona_memories",
+ "tableTo": "geo_personas",
+ "columnsFrom": [
+ "persona_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_persona_memories_organization_id_organizations_id_fk": {
+ "name": "geo_persona_memories_organization_id_organizations_id_fk",
+ "tableFrom": "geo_persona_memories",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_persona_memories_project_id_projects_id_fk": {
+ "name": "geo_persona_memories_project_id_projects_id_fk",
+ "tableFrom": "geo_persona_memories",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_personas": {
+ "name": "geo_personas",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company": {
+ "name": "company",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "summary": {
+ "name": "summary",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "search_style": {
+ "name": "search_style",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile": {
+ "name": "profile",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "conversation_prompts": {
+ "name": "conversation_prompts",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "archived_at": {
+ "name": "archived_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": {
+ "geoPersonas_organizationId_idx": {
+ "name": "geoPersonas_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoPersonas_projectId_idx": {
+ "name": "geoPersonas_projectId_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_personas_organization_id_organizations_id_fk": {
+ "name": "geo_personas_organization_id_organizations_id_fk",
+ "tableFrom": "geo_personas",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_personas_project_id_projects_id_fk": {
+ "name": "geo_personas_project_id_projects_id_fk",
+ "tableFrom": "geo_personas",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_prompt_sequences": {
+ "name": "geo_prompt_sequences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "steps": {
+ "name": "steps",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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": {
+ "geoPromptSequences_organizationId_idx": {
+ "name": "geoPromptSequences_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoPromptSequences_projectId_idx": {
+ "name": "geoPromptSequences_projectId_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_prompt_sequences_organization_id_organizations_id_fk": {
+ "name": "geo_prompt_sequences_organization_id_organizations_id_fk",
+ "tableFrom": "geo_prompt_sequences",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_prompt_sequences_project_id_projects_id_fk": {
+ "name": "geo_prompt_sequences_project_id_projects_id_fk",
+ "tableFrom": "geo_prompt_sequences",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_prompt_suggestions": {
+ "name": "geo_prompt_suggestions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'search_console'"
+ },
+ "source_keywords": {
+ "name": "source_keywords",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "accepted_prompt_id": {
+ "name": "accepted_prompt_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": {
+ "geoPromptSuggestions_organizationId_status_idx": {
+ "name": "geoPromptSuggestions_organizationId_status_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoPromptSuggestions_organizationId_prompt_uidx": {
+ "name": "geoPromptSuggestions_organizationId_prompt_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "prompt",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_prompt_suggestions_organization_id_organizations_id_fk": {
+ "name": "geo_prompt_suggestions_organization_id_organizations_id_fk",
+ "tableFrom": "geo_prompt_suggestions",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_prompt_suggestions_accepted_prompt_id_geo_prompts_id_fk": {
+ "name": "geo_prompt_suggestions_accepted_prompt_id_geo_prompts_id_fk",
+ "tableFrom": "geo_prompt_suggestions",
+ "tableTo": "geo_prompts",
+ "columnsFrom": [
+ "accepted_prompt_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_prompts": {
+ "name": "geo_prompts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "prompt": {
+ "name": "prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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": {
+ "geoPrompts_organizationId_idx": {
+ "name": "geoPrompts_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoPrompts_projectId_idx": {
+ "name": "geoPrompts_projectId_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoPrompts_projectId_createdAt_idx": {
+ "name": "geoPrompts_projectId_createdAt_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_prompts_organization_id_organizations_id_fk": {
+ "name": "geo_prompts_organization_id_organizations_id_fk",
+ "tableFrom": "geo_prompts",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_prompts_project_id_projects_id_fk": {
+ "name": "geo_prompts_project_id_projects_id_fk",
+ "tableFrom": "geo_prompts",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_prospect_reports": {
+ "name": "geo_prospect_reports",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "share_token": {
+ "name": "share_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "company_name": {
+ "name": "company_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "company_domain": {
+ "name": "company_domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "visibility_score": {
+ "name": "visibility_score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "model_count": {
+ "name": "model_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "prompt_count": {
+ "name": "prompt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "report": {
+ "name": "report",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_scanned_at": {
+ "name": "last_scanned_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": {
+ "geoProspectReports_organizationId_idx": {
+ "name": "geoProspectReports_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoProspectReports_shareToken_uidx": {
+ "name": "geoProspectReports_shareToken_uidx",
+ "columns": [
+ {
+ "expression": "share_token",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_prospect_reports_organization_id_organizations_id_fk": {
+ "name": "geo_prospect_reports_organization_id_organizations_id_fk",
+ "tableFrom": "geo_prospect_reports",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_prospect_reports_created_by_user_id_users_id_fk": {
+ "name": "geo_prospect_reports_created_by_user_id_users_id_fk",
+ "tableFrom": "geo_prospect_reports",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_scan_events": {
+ "name": "geo_scan_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "scan_id": {
+ "name": "scan_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "step": {
+ "name": "step",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "engine": {
+ "name": "engine",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "task_key": {
+ "name": "task_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "usage": {
+ "name": "usage",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "geoScanEvents_scanId_startedAt_idx": {
+ "name": "geoScanEvents_scanId_startedAt_idx",
+ "columns": [
+ {
+ "expression": "scan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_scan_events_scan_id_geo_scans_id_fk": {
+ "name": "geo_scan_events_scan_id_geo_scans_id_fk",
+ "tableFrom": "geo_scan_events",
+ "tableTo": "geo_scans",
+ "columnsFrom": [
+ "scan_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_scans": {
+ "name": "geo_scans",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "plan": {
+ "name": "plan",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "plan_summary": {
+ "name": "plan_summary",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_code": {
+ "name": "error_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "failed_stage": {
+ "name": "failed_stage",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "retryable": {
+ "name": "retryable",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "input_tokens": {
+ "name": "input_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "output_tokens": {
+ "name": "output_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cache_read_tokens": {
+ "name": "cache_read_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cache_write_tokens": {
+ "name": "cache_write_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reasoning_tokens": {
+ "name": "reasoning_tokens",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_usd": {
+ "name": "total_usd",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "checks_total": {
+ "name": "checks_total",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "checks_failed": {
+ "name": "checks_failed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mentions": {
+ "name": "mentions",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "usage_by_role": {
+ "name": "usage_by_role",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "geoScans_organizationId_idx": {
+ "name": "geoScans_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoScans_projectId_startedAt_idx": {
+ "name": "geoScans_projectId_startedAt_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_scans_organization_id_organizations_id_fk": {
+ "name": "geo_scans_organization_id_organizations_id_fk",
+ "tableFrom": "geo_scans",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_scans_project_id_projects_id_fk": {
+ "name": "geo_scans_project_id_projects_id_fk",
+ "tableFrom": "geo_scans",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_settings": {
+ "name": "geo_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "company_name": {
+ "name": "company_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "aliases": {
+ "name": "aliases",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "competitors": {
+ "name": "competitors",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "conversion_paths": {
+ "name": "conversion_paths",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "domains": {
+ "name": "domains",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "languages": {
+ "name": "languages",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "engines": {
+ "name": "engines",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enforce_zdr": {
+ "name": "enforce_zdr",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "track_without_search": {
+ "name": "track_without_search",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "non_zdr_approved_engines": {
+ "name": "non_zdr_approved_engines",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "paused_auto_prompt_ids": {
+ "name": "paused_auto_prompt_ids",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "removed_auto_prompt_ids": {
+ "name": "removed_auto_prompt_ids",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "ignored_gap_prompt_ids": {
+ "name": "ignored_gap_prompt_ids",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "ARRAY[]::text[]"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "scan_interval_hours": {
+ "name": "scan_interval_hours",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 24
+ },
+ "sentiment_attempted_at": {
+ "name": "sentiment_attempted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "next_scan_at": {
+ "name": "next_scan_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scan_lease_until": {
+ "name": "scan_lease_until",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scan_started_at": {
+ "name": "scan_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_scan_at": {
+ "name": "last_scan_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": {
+ "geoSettings_organizationId_idx": {
+ "name": "geoSettings_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoSettings_projectId_uidx": {
+ "name": "geoSettings_projectId_uidx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_settings_organization_id_organizations_id_fk": {
+ "name": "geo_settings_organization_id_organizations_id_fk",
+ "tableFrom": "geo_settings",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_settings_project_id_projects_id_fk": {
+ "name": "geo_settings_project_id_projects_id_fk",
+ "tableFrom": "geo_settings",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.geo_shelf_sources": {
+ "name": "geo_shelf_sources",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "domain": {
+ "name": "domain",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kind": {
+ "name": "kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ownership": {
+ "name": "ownership",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "origin": {
+ "name": "origin",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fetch_status": {
+ "name": "fetch_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_fetched_at": {
+ "name": "last_fetched_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "citations": {
+ "name": "citations",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "placements": {
+ "name": "placements",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "opportunity": {
+ "name": "opportunity",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "geoShelfSources_organizationId_idx": {
+ "name": "geoShelfSources_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoShelfSources_projectId_updatedAt_idx": {
+ "name": "geoShelfSources_projectId_updatedAt_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "geoShelfSources_projectId_url_uidx": {
+ "name": "geoShelfSources_projectId_url_uidx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "url",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "geo_shelf_sources_organization_id_organizations_id_fk": {
+ "name": "geo_shelf_sources_organization_id_organizations_id_fk",
+ "tableFrom": "geo_shelf_sources",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_shelf_sources_project_id_projects_id_fk": {
+ "name": "geo_shelf_sources_project_id_projects_id_fk",
+ "tableFrom": "geo_shelf_sources",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "geo_shelf_sources_created_by_user_id_users_id_fk": {
+ "name": "geo_shelf_sources_created_by_user_id_users_id_fk",
+ "tableFrom": "geo_shelf_sources",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.github_app_installations": {
+ "name": "github_app_installations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "installation_id": {
+ "name": "installation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "account_login": {
+ "name": "account_login",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "account_name": {
+ "name": "account_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "account_avatar_url": {
+ "name": "account_avatar_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "account_type": {
+ "name": "account_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "repository_selection": {
+ "name": "repository_selection",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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": {
+ "githubAppInstallations_organizationId_idx": {
+ "name": "githubAppInstallations_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "githubAppInstallations_createdByUserId_idx": {
+ "name": "githubAppInstallations_createdByUserId_idx",
+ "columns": [
+ {
+ "expression": "created_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "githubAppInstallations_organization_installation_uidx": {
+ "name": "githubAppInstallations_organization_installation_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "installation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "github_app_installations_organization_id_organizations_id_fk": {
+ "name": "github_app_installations_organization_id_organizations_id_fk",
+ "tableFrom": "github_app_installations",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "github_app_installations_created_by_user_id_users_id_fk": {
+ "name": "github_app_installations_created_by_user_id_users_id_fk",
+ "tableFrom": "github_app_installations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.github_integrations": {
+ "name": "github_integrations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_token": {
+ "name": "encrypted_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "github_app_installation_id": {
+ "name": "github_app_installation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "github_repository_id": {
+ "name": "github_repository_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "github_repository_private": {
+ "name": "github_repository_private",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner": {
+ "name": "owner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "repo": {
+ "name": "repo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_branch": {
+ "name": "default_branch",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "repository_enabled": {
+ "name": "repository_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "encrypted_webhook_secret": {
+ "name": "encrypted_webhook_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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": {
+ "githubIntegrations_organizationId_idx": {
+ "name": "githubIntegrations_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "githubIntegrations_createdByUserId_idx": {
+ "name": "githubIntegrations_createdByUserId_idx",
+ "columns": [
+ {
+ "expression": "created_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "githubIntegrations_organization_owner_repo_uidx": {
+ "name": "githubIntegrations_organization_owner_repo_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "owner",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repo",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "github_integrations_organization_id_organizations_id_fk": {
+ "name": "github_integrations_organization_id_organizations_id_fk",
+ "tableFrom": "github_integrations",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "github_integrations_created_by_user_id_users_id_fk": {
+ "name": "github_integrations_created_by_user_id_users_id_fk",
+ "tableFrom": "github_integrations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "github_integrations_github_app_installation_id_github_app_installations_id_fk": {
+ "name": "github_integrations_github_app_installation_id_github_app_installations_id_fk",
+ "tableFrom": "github_integrations",
+ "tableTo": "github_app_installations",
+ "columnsFrom": [
+ "github_app_installation_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.google_search_console_integrations": {
+ "name": "google_search_console_integrations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "google_account_email": {
+ "name": "google_account_email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_access_token": {
+ "name": "encrypted_access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_refresh_token": {
+ "name": "encrypted_refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "site_url": {
+ "name": "site_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "qstash_schedule_id": {
+ "name": "qstash_schedule_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "disconnecting_at": {
+ "name": "disconnecting_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "top_queries": {
+ "name": "top_queries",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "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": {
+ "googleSearchConsoleIntegrations_organizationId_uidx": {
+ "name": "googleSearchConsoleIntegrations_organizationId_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "googleSearchConsoleIntegrations_createdByUserId_idx": {
+ "name": "googleSearchConsoleIntegrations_createdByUserId_idx",
+ "columns": [
+ {
+ "expression": "created_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "google_search_console_integrations_organization_id_organizations_id_fk": {
+ "name": "google_search_console_integrations_organization_id_organizations_id_fk",
+ "tableFrom": "google_search_console_integrations",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "google_search_console_integrations_created_by_user_id_users_id_fk": {
+ "name": "google_search_console_integrations_created_by_user_id_users_id_fk",
+ "tableFrom": "google_search_console_integrations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.granola_integrations": {
+ "name": "granola_integrations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_api_key": {
+ "name": "encrypted_api_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "workspace_name": {
+ "name": "workspace_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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": {
+ "granolaIntegrations_organizationId_idx": {
+ "name": "granolaIntegrations_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "granolaIntegrations_createdByUserId_idx": {
+ "name": "granolaIntegrations_createdByUserId_idx",
+ "columns": [
+ {
+ "expression": "created_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "granola_integrations_organization_id_organizations_id_fk": {
+ "name": "granola_integrations_organization_id_organizations_id_fk",
+ "tableFrom": "granola_integrations",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "granola_integrations_created_by_user_id_users_id_fk": {
+ "name": "granola_integrations_created_by_user_id_users_id_fk",
+ "tableFrom": "granola_integrations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.linear_integrations": {
+ "name": "linear_integrations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_access_token": {
+ "name": "encrypted_access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "linear_organization_id": {
+ "name": "linear_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "linear_organization_name": {
+ "name": "linear_organization_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "linear_team_id": {
+ "name": "linear_team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "linear_team_name": {
+ "name": "linear_team_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_webhook_secret": {
+ "name": "encrypted_webhook_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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": {
+ "linearIntegrations_organizationId_idx": {
+ "name": "linearIntegrations_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "linearIntegrations_createdByUserId_idx": {
+ "name": "linearIntegrations_createdByUserId_idx",
+ "columns": [
+ {
+ "expression": "created_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "linearIntegrations_org_linearOrg_team_uidx": {
+ "name": "linearIntegrations_org_linearOrg_team_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "linear_organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "linear_team_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "linearIntegrations_org_linearOrg_no_team_uidx": {
+ "name": "linearIntegrations_org_linearOrg_no_team_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "linear_organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"linear_integrations\".\"linear_team_id\" IS NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "linear_integrations_organization_id_organizations_id_fk": {
+ "name": "linear_integrations_organization_id_organizations_id_fk",
+ "tableFrom": "linear_integrations",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "linear_integrations_created_by_user_id_users_id_fk": {
+ "name": "linear_integrations_created_by_user_id_users_id_fk",
+ "tableFrom": "linear_integrations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_oauth_credentials": {
+ "name": "mcp_oauth_credentials",
+ "schema": "",
+ "columns": {
+ "server_integration_id": {
+ "name": "server_integration_id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connected_by_user_id": {
+ "name": "connected_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_tokens": {
+ "name": "encrypted_tokens",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_client_information": {
+ "name": "encrypted_client_information",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_authorization_server_information": {
+ "name": "encrypted_authorization_server_information",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_refresh_at": {
+ "name": "access_token_refresh_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'connected'"
+ },
+ "token_version": {
+ "name": "token_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "refresh_lease_id": {
+ "name": "refresh_lease_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_lease_expires_at": {
+ "name": "refresh_lease_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_refreshed_at": {
+ "name": "last_refreshed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "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": {
+ "mcpOAuthCredentials_organizationId_idx": {
+ "name": "mcpOAuthCredentials_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpOAuthCredentials_connectedByUserId_idx": {
+ "name": "mcpOAuthCredentials_connectedByUserId_idx",
+ "columns": [
+ {
+ "expression": "connected_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_oauth_credentials_server_integration_id_mcp_server_integrations_id_fk": {
+ "name": "mcp_oauth_credentials_server_integration_id_mcp_server_integrations_id_fk",
+ "tableFrom": "mcp_oauth_credentials",
+ "tableTo": "mcp_server_integrations",
+ "columnsFrom": [
+ "server_integration_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_oauth_credentials_organization_id_organizations_id_fk": {
+ "name": "mcp_oauth_credentials_organization_id_organizations_id_fk",
+ "tableFrom": "mcp_oauth_credentials",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_oauth_credentials_connected_by_user_id_users_id_fk": {
+ "name": "mcp_oauth_credentials_connected_by_user_id_users_id_fk",
+ "tableFrom": "mcp_oauth_credentials",
+ "tableTo": "users",
+ "columnsFrom": [
+ "connected_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcpOAuthCredentials_org_server_fk": {
+ "name": "mcpOAuthCredentials_org_server_fk",
+ "tableFrom": "mcp_oauth_credentials",
+ "tableTo": "mcp_server_integrations",
+ "columnsFrom": [
+ "organization_id",
+ "server_integration_id"
+ ],
+ "columnsTo": [
+ "organization_id",
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "mcpOAuthCredentials_status_check": {
+ "name": "mcpOAuthCredentials_status_check",
+ "value": "\"mcp_oauth_credentials\".\"status\" IN ('connected', 'refreshing', 'reauth_required')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.mcp_oauth_pending_authorizations": {
+ "name": "mcp_oauth_pending_authorizations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "server_integration_id": {
+ "name": "server_integration_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "store_source_integration_id": {
+ "name": "store_source_integration_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "callback_path": {
+ "name": "callback_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "state_hash": {
+ "name": "state_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_state": {
+ "name": "encrypted_state",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_code_verifier": {
+ "name": "encrypted_code_verifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_client_information": {
+ "name": "encrypted_client_information",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "encrypted_authorization_server_information": {
+ "name": "encrypted_authorization_server_information",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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": {
+ "mcpOAuthPendingAuthorizations_organizationId_idx": {
+ "name": "mcpOAuthPendingAuthorizations_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpOAuthPendingAuthorizations_userId_idx": {
+ "name": "mcpOAuthPendingAuthorizations_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpOAuthPendingAuthorizations_serverIntegrationId_idx": {
+ "name": "mcpOAuthPendingAuthorizations_serverIntegrationId_idx",
+ "columns": [
+ {
+ "expression": "server_integration_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpOAuthPendingAuthorizations_storeSourceIntegrationId_idx": {
+ "name": "mcpOAuthPendingAuthorizations_storeSourceIntegrationId_idx",
+ "columns": [
+ {
+ "expression": "store_source_integration_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpOAuthPendingAuthorizations_expiresAt_idx": {
+ "name": "mcpOAuthPendingAuthorizations_expiresAt_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_oauth_pending_authorizations_organization_id_organizations_id_fk": {
+ "name": "mcp_oauth_pending_authorizations_organization_id_organizations_id_fk",
+ "tableFrom": "mcp_oauth_pending_authorizations",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_oauth_pending_authorizations_user_id_users_id_fk": {
+ "name": "mcp_oauth_pending_authorizations_user_id_users_id_fk",
+ "tableFrom": "mcp_oauth_pending_authorizations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_oauth_pending_authorizations_server_integration_id_mcp_server_integrations_id_fk": {
+ "name": "mcp_oauth_pending_authorizations_server_integration_id_mcp_server_integrations_id_fk",
+ "tableFrom": "mcp_oauth_pending_authorizations",
+ "tableTo": "mcp_server_integrations",
+ "columnsFrom": [
+ "server_integration_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcpOAuthPendingAuthorizations_org_server_fk": {
+ "name": "mcpOAuthPendingAuthorizations_org_server_fk",
+ "tableFrom": "mcp_oauth_pending_authorizations",
+ "tableTo": "mcp_server_integrations",
+ "columnsFrom": [
+ "organization_id",
+ "server_integration_id"
+ ],
+ "columnsTo": [
+ "organization_id",
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcpOAuthPendingAuthorizations_storeSourceIntegrationId_fk": {
+ "name": "mcpOAuthPendingAuthorizations_storeSourceIntegrationId_fk",
+ "tableFrom": "mcp_oauth_pending_authorizations",
+ "tableTo": "mcp_server_integrations",
+ "columnsFrom": [
+ "store_source_integration_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "mcp_oauth_pending_authorizations_state_hash_unique": {
+ "name": "mcp_oauth_pending_authorizations_state_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "state_hash"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_server_integrations": {
+ "name": "mcp_server_integrations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resource_type": {
+ "name": "resource_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'connection'"
+ },
+ "author": {
+ "name": "author",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "website_url": {
+ "name": "website_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "brand_color": {
+ "name": "brand_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "logo_light_url": {
+ "name": "logo_light_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "logo_dark_url": {
+ "name": "logo_dark_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "banner_url": {
+ "name": "banner_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "category": {
+ "name": "category",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "store_featured_at": {
+ "name": "store_featured_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "store_source_integration_id": {
+ "name": "store_source_integration_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "store_status": {
+ "name": "store_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "review_note": {
+ "name": "review_note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "submitted_at": {
+ "name": "submitted_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviewed_at": {
+ "name": "reviewed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auth_type": {
+ "name": "auth_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "encrypted_headers": {
+ "name": "encrypted_headers",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "last_tool_sync_at": {
+ "name": "last_tool_sync_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tool_sync_status": {
+ "name": "tool_sync_status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'idle'"
+ },
+ "tool_sync_error": {
+ "name": "tool_sync_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "indexed_tool_count": {
+ "name": "indexed_tool_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "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": {
+ "mcpServerIntegrations_resourceType_idx": {
+ "name": "mcpServerIntegrations_resourceType_idx",
+ "columns": [
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpServerIntegrations_storeStatus_idx": {
+ "name": "mcpServerIntegrations_storeStatus_idx",
+ "columns": [
+ {
+ "expression": "store_status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpServerIntegrations_organizationId_idx": {
+ "name": "mcpServerIntegrations_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpServerIntegrations_createdByUserId_idx": {
+ "name": "mcpServerIntegrations_createdByUserId_idx",
+ "columns": [
+ {
+ "expression": "created_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpServerIntegrations_storeSourceIntegrationId_idx": {
+ "name": "mcpServerIntegrations_storeSourceIntegrationId_idx",
+ "columns": [
+ {
+ "expression": "store_source_integration_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpServerIntegrations_org_id_uidx": {
+ "name": "mcpServerIntegrations_org_id_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpServerIntegrations_org_resourceType_name_uidx": {
+ "name": "mcpServerIntegrations_org_resourceType_name_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpServerIntegrations_org_storeSource_uidx": {
+ "name": "mcpServerIntegrations_org_storeSource_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "store_source_integration_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"mcp_server_integrations\".\"store_source_integration_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpServerIntegrations_storeListing_slug_uidx": {
+ "name": "mcpServerIntegrations_storeListing_slug_uidx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"mcp_server_integrations\".\"resource_type\" = 'store_listing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_server_integrations_organization_id_organizations_id_fk": {
+ "name": "mcp_server_integrations_organization_id_organizations_id_fk",
+ "tableFrom": "mcp_server_integrations",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_server_integrations_created_by_user_id_users_id_fk": {
+ "name": "mcp_server_integrations_created_by_user_id_users_id_fk",
+ "tableFrom": "mcp_server_integrations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcpServerIntegrations_storeSourceIntegrationId_fk": {
+ "name": "mcpServerIntegrations_storeSourceIntegrationId_fk",
+ "tableFrom": "mcp_server_integrations",
+ "tableTo": "mcp_server_integrations",
+ "columnsFrom": [
+ "store_source_integration_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "mcpServerIntegrations_authType_check": {
+ "name": "mcpServerIntegrations_authType_check",
+ "value": "\"mcp_server_integrations\".\"auth_type\" IN ('none', 'headers', 'oauth')"
+ },
+ "mcpServerIntegrations_storeStatus_check": {
+ "name": "mcpServerIntegrations_storeStatus_check",
+ "value": "\"mcp_server_integrations\".\"store_status\" IN ('draft', 'pending_review', 'live', 'rejected')"
+ },
+ "mcpServerIntegrations_resourceType_check": {
+ "name": "mcpServerIntegrations_resourceType_check",
+ "value": "\"mcp_server_integrations\".\"resource_type\" IN ('connection', 'store_listing')"
+ },
+ "mcpServerIntegrations_category_check": {
+ "name": "mcpServerIntegrations_category_check",
+ "value": "\"mcp_server_integrations\".\"category\" IS NULL OR \"mcp_server_integrations\".\"category\" IN ('AI', 'Source control', 'Project management', 'Communication', 'Design', 'Notes', 'Deploys', 'Productivity', 'Marketing', 'Publishing')"
+ },
+ "mcpServerIntegrations_resourceState_check": {
+ "name": "mcpServerIntegrations_resourceState_check",
+ "value": "(\n (\"mcp_server_integrations\".\"resource_type\" = 'store_listing' AND \"mcp_server_integrations\".\"store_source_integration_id\" IS NULL)\n OR\n (\"mcp_server_integrations\".\"resource_type\" = 'connection' AND \"mcp_server_integrations\".\"store_status\" = 'draft' AND \"mcp_server_integrations\".\"review_note\" IS NULL AND \"mcp_server_integrations\".\"submitted_at\" IS NULL AND \"mcp_server_integrations\".\"reviewed_at\" IS NULL)\n )"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.mcp_session_tool_activations": {
+ "name": "mcp_session_tool_activations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "surface": {
+ "name": "surface",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "mcp_tool_index_id": {
+ "name": "mcp_tool_index_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "runtime_tool_name": {
+ "name": "runtime_tool_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_query": {
+ "name": "source_query",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "activated_at": {
+ "name": "activated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "mcpSessionToolActivations_session_tool_uidx": {
+ "name": "mcpSessionToolActivations_session_tool_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "surface",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "mcp_tool_index_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpSessionToolActivations_session_idx": {
+ "name": "mcpSessionToolActivations_session_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "surface",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpSessionToolActivations_expiresAt_idx": {
+ "name": "mcpSessionToolActivations_expiresAt_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_session_tool_activations_organization_id_organizations_id_fk": {
+ "name": "mcp_session_tool_activations_organization_id_organizations_id_fk",
+ "tableFrom": "mcp_session_tool_activations",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_session_tool_activations_mcp_tool_index_id_mcp_tool_index_id_fk": {
+ "name": "mcp_session_tool_activations_mcp_tool_index_id_mcp_tool_index_id_fk",
+ "tableFrom": "mcp_session_tool_activations",
+ "tableTo": "mcp_tool_index",
+ "columnsFrom": [
+ "mcp_tool_index_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcpSessionToolActivations_org_tool_fk": {
+ "name": "mcpSessionToolActivations_org_tool_fk",
+ "tableFrom": "mcp_session_tool_activations",
+ "tableTo": "mcp_tool_index",
+ "columnsFrom": [
+ "organization_id",
+ "mcp_tool_index_id"
+ ],
+ "columnsTo": [
+ "organization_id",
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.mcp_tool_index": {
+ "name": "mcp_tool_index",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "server_integration_id": {
+ "name": "server_integration_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "server_tool_name": {
+ "name": "server_tool_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "runtime_tool_name": {
+ "name": "runtime_tool_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action_phrase_present": {
+ "name": "action_phrase_present",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action_phrase_past": {
+ "name": "action_phrase_past",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "input_schema": {
+ "name": "input_schema",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "output_schema": {
+ "name": "output_schema",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "annotations": {
+ "name": "annotations",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "meta": {
+ "name": "meta",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "schema_hash": {
+ "name": "schema_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "search_text": {
+ "name": "search_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'active'"
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_indexed_at": {
+ "name": "last_indexed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "error_message": {
+ "name": "error_message",
+ "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": {
+ "mcpToolIndex_server_tool_uidx": {
+ "name": "mcpToolIndex_server_tool_uidx",
+ "columns": [
+ {
+ "expression": "server_integration_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "server_tool_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpToolIndex_org_id_uidx": {
+ "name": "mcpToolIndex_org_id_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpToolIndex_org_runtime_tool_uidx": {
+ "name": "mcpToolIndex_org_runtime_tool_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "runtime_tool_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpToolIndex_organizationId_status_idx": {
+ "name": "mcpToolIndex_organizationId_status_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpToolIndex_serverIntegrationId_status_idx": {
+ "name": "mcpToolIndex_serverIntegrationId_status_idx",
+ "columns": [
+ {
+ "expression": "server_integration_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "mcpToolIndex_searchText_gin_idx": {
+ "name": "mcpToolIndex_searchText_gin_idx",
+ "columns": [
+ {
+ "expression": "to_tsvector('english', \"search_text\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "mcp_tool_index_organization_id_organizations_id_fk": {
+ "name": "mcp_tool_index_organization_id_organizations_id_fk",
+ "tableFrom": "mcp_tool_index",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcp_tool_index_server_integration_id_mcp_server_integrations_id_fk": {
+ "name": "mcp_tool_index_server_integration_id_mcp_server_integrations_id_fk",
+ "tableFrom": "mcp_tool_index",
+ "tableTo": "mcp_server_integrations",
+ "columnsFrom": [
+ "server_integration_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "mcpToolIndex_org_server_fk": {
+ "name": "mcpToolIndex_org_server_fk",
+ "tableFrom": "mcp_tool_index",
+ "tableTo": "mcp_server_integrations",
+ "columnsFrom": [
+ "organization_id",
+ "server_integration_id"
+ ],
+ "columnsTo": [
+ "organization_id",
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.members": {
+ "name": "members",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "members_organizationId_idx": {
+ "name": "members_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "members_userId_idx": {
+ "name": "members_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "members_organizationId_userId_uidx": {
+ "name": "members_organizationId_userId_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "members_organization_id_organizations_id_fk": {
+ "name": "members_organization_id_organizations_id_fk",
+ "tableFrom": "members",
+ "tableTo": "organizations",
+ "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",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.onboarding_suggestions": {
+ "name": "onboarding_suggestions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "onboarding_suggestion_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dismissed": {
+ "name": "dismissed",
+ "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": {
+ "onboardingSuggestions_org_type_idx": {
+ "name": "onboardingSuggestions_org_type_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "onboarding_suggestions_organization_id_organizations_id_fk": {
+ "name": "onboarding_suggestions_organization_id_organizations_id_fk",
+ "tableFrom": "onboarding_suggestions",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organization_notification_settings": {
+ "name": "organization_notification_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scheduled_content_creation": {
+ "name": "scheduled_content_creation",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "scheduled_content_failed": {
+ "name": "scheduled_content_failed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "scheduled_content_skipped": {
+ "name": "scheduled_content_skipped",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "marketing_emails": {
+ "name": "marketing_emails",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "daily_summary": {
+ "name": "daily_summary",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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": {
+ "orgNotificationSettings_organizationId_uidx": {
+ "name": "orgNotificationSettings_organizationId_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "organization_notification_settings_organization_id_organizations_id_fk": {
+ "name": "organization_notification_settings_organization_id_organizations_id_fk",
+ "tableFrom": "organization_notification_settings",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organizations": {
+ "name": "organizations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "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
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "heard_about_notra_source": {
+ "name": "heard_about_notra_source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "heard_about_notra_other": {
+ "name": "heard_about_notra_other",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "geo_ingest_token_generation": {
+ "name": "geo_ingest_token_generation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "feedback_ingest_token_generation": {
+ "name": "feedback_ingest_token_generation",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "onboarding_completed": {
+ "name": "onboarding_completed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "onboarding_dismissed": {
+ "name": "onboarding_dismissed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "onboarding_agent_ran": {
+ "name": "onboarding_agent_ran",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "onboarding_agent_started_at": {
+ "name": "onboarding_agent_started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workos_org_id": {
+ "name": "workos_org_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "organizations_slug_unique": {
+ "name": "organizations_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ },
+ "organizations_workos_org_id_unique": {
+ "name": "organizations_workos_org_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "workos_org_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.post_collections": {
+ "name": "post_collections",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "source": {
+ "name": "source",
+ "type": "post_collection_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name_source": {
+ "name": "name_source",
+ "type": "post_collection_name_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'generated'"
+ },
+ "content_types": {
+ "name": "content_types",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "source_metadata": {
+ "name": "source_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expected_post_count": {
+ "name": "expected_post_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "completed_post_count": {
+ "name": "completed_post_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "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": {
+ "post_collections_org_created_at_idx": {
+ "name": "post_collections_org_created_at_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "post_collections_source_idx": {
+ "name": "post_collections_source_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "post_collections_org_project_idx": {
+ "name": "post_collections_org_project_idx",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "post_collections_chat_source_uidx": {
+ "name": "post_collections_chat_source_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"post_collections\".\"source\" = 'chat' AND \"post_collections\".\"source_id\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "post_collections_organization_id_organizations_id_fk": {
+ "name": "post_collections_organization_id_organizations_id_fk",
+ "tableFrom": "post_collections",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "post_collections_project_id_projects_id_fk": {
+ "name": "post_collections_project_id_projects_id_fk",
+ "tableFrom": "post_collections",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.posts": {
+ "name": "posts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "collection_id": {
+ "name": "collection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "html_url": {
+ "name": "html_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "markdown": {
+ "name": "markdown",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "recommendations": {
+ "name": "recommendations",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_subtype": {
+ "name": "content_subtype",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "source_metadata": {
+ "name": "source_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "github_publish": {
+ "name": "github_publish",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "post_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "posts_org_slug_uidx": {
+ "name": "posts_org_slug_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"posts\".\"slug\" IS NOT NULL",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_org_createdAt_id_idx": {
+ "name": "posts_org_createdAt_id_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_collection_id_idx": {
+ "name": "posts_collection_id_idx",
+ "columns": [
+ {
+ "expression": "collection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_org_createdAt_status_idx": {
+ "name": "posts_org_createdAt_status_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_org_published_createdAt_idx": {
+ "name": "posts_org_published_createdAt_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"posts\".\"status\" = 'published'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "posts_org_content_type_updated_at_idx": {
+ "name": "posts_org_content_type_updated_at_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "content_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": false,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "posts_organization_id_organizations_id_fk": {
+ "name": "posts_organization_id_organizations_id_fk",
+ "tableFrom": "posts",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "posts_collection_id_post_collections_id_fk": {
+ "name": "posts_collection_id_post_collections_id_fk",
+ "tableFrom": "posts",
+ "tableTo": "post_collections",
+ "columnsFrom": [
+ "collection_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.projects": {
+ "name": "projects",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "brand_settings_id": {
+ "name": "brand_settings_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_sample": {
+ "name": "is_sample",
+ "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": {
+ "projects_organizationId_idx": {
+ "name": "projects_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "projects_organizationId_sample_uidx": {
+ "name": "projects_organizationId_sample_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"projects\".\"is_sample\" = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "projects_organization_id_organizations_id_fk": {
+ "name": "projects_organization_id_organizations_id_fk",
+ "tableFrom": "projects",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "projects_brand_settings_id_brand_settings_id_fk": {
+ "name": "projects_brand_settings_id_brand_settings_id_fk",
+ "tableFrom": "projects",
+ "tableTo": "brand_settings",
+ "columnsFrom": [
+ "brand_settings_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.repository_outputs": {
+ "name": "repository_outputs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "repository_id": {
+ "name": "repository_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "output_type": {
+ "name": "output_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "repositoryOutputs_repositoryId_idx": {
+ "name": "repositoryOutputs_repositoryId_idx",
+ "columns": [
+ {
+ "expression": "repository_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "repositoryOutputs_repository_outputType_uidx": {
+ "name": "repositoryOutputs_repository_outputType_uidx",
+ "columns": [
+ {
+ "expression": "repository_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "output_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "repository_outputs_repository_id_github_integrations_id_fk": {
+ "name": "repository_outputs_repository_id_github_integrations_id_fk",
+ "tableFrom": "repository_outputs",
+ "tableTo": "github_integrations",
+ "columnsFrom": [
+ "repository_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.skills": {
+ "name": "skills",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_system": {
+ "name": "is_system",
+ "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": {
+ "skills_organizationId_idx": {
+ "name": "skills_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "skills_org_name_uidx": {
+ "name": "skills_org_name_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "skills_organization_id_organizations_id_fk": {
+ "name": "skills_organization_id_organizations_id_fk",
+ "tableFrom": "skills",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.slack_integrations": {
+ "name": "slack_integrations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_bot_token": {
+ "name": "encrypted_bot_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slack_team_id": {
+ "name": "slack_team_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slack_team_name": {
+ "name": "slack_team_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "slack_bot_user_id": {
+ "name": "slack_bot_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "allowed_channel_ids": {
+ "name": "allowed_channel_ids",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notification_channel_id": {
+ "name": "notification_channel_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 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": {
+ "slackIntegrations_organizationId_idx": {
+ "name": "slackIntegrations_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slackIntegrations_createdByUserId_idx": {
+ "name": "slackIntegrations_createdByUserId_idx",
+ "columns": [
+ {
+ "expression": "created_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "slackIntegrations_teamId_uidx": {
+ "name": "slackIntegrations_teamId_uidx",
+ "columns": [
+ {
+ "expression": "slack_team_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "slack_integrations_organization_id_organizations_id_fk": {
+ "name": "slack_integrations_organization_id_organizations_id_fk",
+ "tableFrom": "slack_integrations",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "slack_integrations_created_by_user_id_users_id_fk": {
+ "name": "slack_integrations_created_by_user_id_users_id_fk",
+ "tableFrom": "slack_integrations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.social_connections": {
+ "name": "social_connections",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_account_id": {
+ "name": "provider_account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_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()"
+ }
+ },
+ "indexes": {
+ "socialConnections_userId_provider_uidx": {
+ "name": "socialConnections_userId_provider_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "socialConnections_userId_idx": {
+ "name": "socialConnections_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "socialConnections_provider_providerAccountId_idx": {
+ "name": "socialConnections_provider_providerAccountId_idx",
+ "columns": [
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "social_connections_user_id_users_id_fk": {
+ "name": "social_connections_user_id_users_id_fk",
+ "tableFrom": "social_connections",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.social_experiments": {
+ "name": "social_experiments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hypothesis": {
+ "name": "hypothesis",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "variant_a_post_id": {
+ "name": "variant_a_post_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "variant_b_post_id": {
+ "name": "variant_b_post_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metric": {
+ "name": "metric",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "winner": {
+ "name": "winner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "ended_at": {
+ "name": "ended_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": {
+ "socialExperiments_organizationId_idx": {
+ "name": "socialExperiments_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "social_experiments_organization_id_organizations_id_fk": {
+ "name": "social_experiments_organization_id_organizations_id_fk",
+ "tableFrom": "social_experiments",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tracked_social_accounts": {
+ "name": "tracked_social_accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_account_id": {
+ "name": "provider_account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "profile_image_url": {
+ "name": "profile_image_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "verified": {
+ "name": "verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "verified_type": {
+ "name": "verified_type",
+ "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": {
+ "trackedSocialAccounts_organizationId_idx": {
+ "name": "trackedSocialAccounts_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "trackedSocialAccounts_org_provider_account_uidx": {
+ "name": "trackedSocialAccounts_org_provider_account_uidx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_account_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tracked_social_accounts_organization_id_organizations_id_fk": {
+ "name": "tracked_social_accounts_organization_id_organizations_id_fk",
+ "tableFrom": "tracked_social_accounts",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_backup_codes": {
+ "name": "user_backup_codes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "code_hash": {
+ "name": "code_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "user_backup_codes_userId_idx": {
+ "name": "user_backup_codes_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_backup_codes_userId_codeHash_uidx": {
+ "name": "user_backup_codes_userId_codeHash_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "code_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_backup_codes_user_id_users_id_fk": {
+ "name": "user_backup_codes_user_id_users_id_fk",
+ "tableFrom": "user_backup_codes",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "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()"
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "banned": {
+ "name": "banned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "default": false
+ },
+ "ban_reason": {
+ "name": "ban_reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ban_expires": {
+ "name": "ban_expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hide_personal_data": {
+ "name": "hide_personal_data",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "show_agent_stats": {
+ "name": "show_agent_stats",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "workos_user_id": {
+ "name": "workos_user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ },
+ "users_workos_user_id_unique": {
+ "name": "users_workos_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "workos_user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.applicable_platform": {
+ "name": "applicable_platform",
+ "schema": "public",
+ "values": [
+ "all",
+ "twitter",
+ "linkedin",
+ "blog"
+ ]
+ },
+ "public.autonomy_action_status": {
+ "name": "autonomy_action_status",
+ "schema": "public",
+ "values": [
+ "pending",
+ "executing",
+ "succeeded",
+ "failed",
+ "unknown",
+ "compensated",
+ "canceled"
+ ]
+ },
+ "public.autonomy_goal_status": {
+ "name": "autonomy_goal_status",
+ "schema": "public",
+ "values": [
+ "open",
+ "in_progress",
+ "blocked",
+ "completed",
+ "abandoned"
+ ]
+ },
+ "public.autonomy_mandate_status": {
+ "name": "autonomy_mandate_status",
+ "schema": "public",
+ "values": [
+ "active",
+ "paused",
+ "revoked"
+ ]
+ },
+ "public.autonomy_outbox_status": {
+ "name": "autonomy_outbox_status",
+ "schema": "public",
+ "values": [
+ "pending",
+ "attempting",
+ "delivered",
+ "failed",
+ "canceled"
+ ]
+ },
+ "public.autonomy_run_status": {
+ "name": "autonomy_run_status",
+ "schema": "public",
+ "values": [
+ "planning",
+ "executing",
+ "completed",
+ "failed",
+ "canceled"
+ ]
+ },
+ "public.autonomy_run_trigger": {
+ "name": "autonomy_run_trigger",
+ "schema": "public",
+ "values": [
+ "signal",
+ "wake",
+ "manual",
+ "repair"
+ ]
+ },
+ "public.autonomy_signal_status": {
+ "name": "autonomy_signal_status",
+ "schema": "public",
+ "values": [
+ "pending",
+ "coalesced",
+ "processed",
+ "discarded"
+ ]
+ },
+ "public.autonomy_task_status": {
+ "name": "autonomy_task_status",
+ "schema": "public",
+ "values": [
+ "pending",
+ "ready",
+ "running",
+ "waiting",
+ "completed",
+ "failed",
+ "canceled"
+ ]
+ },
+ "public.brand_guideline_asset_kind": {
+ "name": "brand_guideline_asset_kind",
+ "schema": "public",
+ "values": [
+ "logo",
+ "wordmark"
+ ]
+ },
+ "public.brand_guideline_asset_variant": {
+ "name": "brand_guideline_asset_variant",
+ "schema": "public",
+ "values": [
+ "light",
+ "dark"
+ ]
+ },
+ "public.brand_guideline_color_role": {
+ "name": "brand_guideline_color_role",
+ "schema": "public",
+ "values": [
+ "primary",
+ "secondary",
+ "accent",
+ "background",
+ "foreground",
+ "neutral",
+ "custom"
+ ]
+ },
+ "public.brand_guideline_font_role": {
+ "name": "brand_guideline_font_role",
+ "schema": "public",
+ "values": [
+ "heading",
+ "body",
+ "button",
+ "unknown"
+ ]
+ },
+ "public.brand_guideline_screenshot_kind": {
+ "name": "brand_guideline_screenshot_kind",
+ "schema": "public",
+ "values": [
+ "desktop_hero",
+ "desktop_full_page",
+ "mobile_hero"
+ ]
+ },
+ "public.brand_guideline_status": {
+ "name": "brand_guideline_status",
+ "schema": "public",
+ "values": [
+ "queued",
+ "generating",
+ "ready",
+ "failed"
+ ]
+ },
+ "public.brand_guideline_token_type": {
+ "name": "brand_guideline_token_type",
+ "schema": "public",
+ "values": [
+ "spacing",
+ "radius",
+ "shadow",
+ "component",
+ "unknown"
+ ]
+ },
+ "public.brand_sitemap_page_category": {
+ "name": "brand_sitemap_page_category",
+ "schema": "public",
+ "values": [
+ "crawled",
+ "redirect",
+ "queued",
+ "failed"
+ ]
+ },
+ "public.brand_sitemap_status": {
+ "name": "brand_sitemap_status",
+ "schema": "public",
+ "values": [
+ "queued",
+ "crawling",
+ "ready",
+ "failed"
+ ]
+ },
+ "public.lookback_window": {
+ "name": "lookback_window",
+ "schema": "public",
+ "values": [
+ "current_day",
+ "yesterday",
+ "last_7_days",
+ "last_14_days",
+ "last_30_days"
+ ]
+ },
+ "public.onboarding_suggestion_type": {
+ "name": "onboarding_suggestion_type",
+ "schema": "public",
+ "values": [
+ "schedule_automation",
+ "event_automation"
+ ]
+ },
+ "public.post_collection_name_source": {
+ "name": "post_collection_name_source",
+ "schema": "public",
+ "values": [
+ "generated",
+ "user",
+ "backfill"
+ ]
+ },
+ "public.post_collection_source": {
+ "name": "post_collection_source",
+ "schema": "public",
+ "values": [
+ "manual",
+ "chat",
+ "schedule",
+ "automation",
+ "api",
+ "backfill"
+ ]
+ },
+ "public.post_status": {
+ "name": "post_status",
+ "schema": "public",
+ "values": [
+ "draft",
+ "published"
+ ]
+ },
+ "public.reference_type": {
+ "name": "reference_type",
+ "schema": "public",
+ "values": [
+ "twitter_post",
+ "linkedin_post",
+ "blog_post",
+ "custom"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json
index 0f189a613..0122698c8 100644
--- a/packages/db/migrations/meta/_journal.json
+++ b/packages/db/migrations/meta/_journal.json
@@ -701,6 +701,13 @@
"when": 1790019760563,
"tag": "0099_sticky_triathlon",
"breakpoints": true
+ },
+ {
+ "idx": 100,
+ "version": "7",
+ "when": 1790112228943,
+ "tag": "0100_complete_robin_chapel",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts
index e2eff9505..bc363be47 100644
--- a/packages/db/src/schema.ts
+++ b/packages/db/src/schema.ts
@@ -85,7 +85,7 @@ export const users = pgTable("users", {
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
role: text("role"),
banned: boolean("banned").default(false),
@@ -96,6 +96,26 @@ export const users = pgTable("users", {
workosUserId: text("workos_user_id").unique(),
});
+export const userBackupCodes = pgTable(
+ "user_backup_codes",
+ {
+ id: text("id").primaryKey(),
+ userId: text("user_id")
+ .notNull()
+ .references(() => users.id, { onDelete: "cascade" }),
+ codeHash: text("code_hash").notNull(),
+ usedAt: timestamp("used_at"),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ },
+ (table) => [
+ index("user_backup_codes_userId_idx").on(table.userId),
+ uniqueIndex("user_backup_codes_userId_codeHash_uidx").on(
+ table.userId,
+ table.codeHash
+ ),
+ ]
+);
+
export const chatSessions = pgTable(
"chat_sessions",
{
@@ -120,7 +140,7 @@ export const chatSessions = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -172,7 +192,7 @@ export const agentSessions = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -223,7 +243,7 @@ export const socialConnections = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -239,39 +259,27 @@ export const socialConnections = pgTable(
]
);
-export const organizations = pgTable(
- "organizations",
- {
- id: text("id").primaryKey(),
- name: text("name").notNull(),
- slug: text("slug").notNull().unique(),
- logo: text("logo"),
- createdAt: timestamp("created_at").notNull(),
- metadata: text("metadata"),
- heardAboutNotraSource: text("heard_about_notra_source"),
- heardAboutNotraOther: text("heard_about_notra_other"),
- geoIngestTokenGeneration: integer("geo_ingest_token_generation")
- .notNull()
- .default(1),
- feedbackIngestTokenGeneration: integer("feedback_ingest_token_generation")
- .notNull()
- .default(1),
- onboardingCompleted: boolean("onboarding_completed")
- .default(false)
- .notNull(),
- onboardingDismissed: boolean("onboarding_dismissed")
- .default(false)
- .notNull(),
- onboardingAgentRan: boolean("onboarding_agent_ran")
- .default(false)
- .notNull(),
- onboardingAgentStartedAt: timestamp("onboarding_agent_started_at"),
- workosOrgId: text("workos_org_id").unique(),
- }
- // No extra indexes: `slug` already carries a unique constraint
- // (`organizations_slug_unique`) that Postgres backs with a unique index, so a
- // second identical index would only double index maintenance on every write.
-);
+export const organizations = pgTable("organizations", {
+ id: text("id").primaryKey(),
+ name: text("name").notNull(),
+ slug: text("slug").notNull().unique(),
+ logo: text("logo"),
+ createdAt: timestamp("created_at").notNull(),
+ metadata: text("metadata"),
+ heardAboutNotraSource: text("heard_about_notra_source"),
+ heardAboutNotraOther: text("heard_about_notra_other"),
+ geoIngestTokenGeneration: integer("geo_ingest_token_generation")
+ .notNull()
+ .default(1),
+ feedbackIngestTokenGeneration: integer("feedback_ingest_token_generation")
+ .notNull()
+ .default(1),
+ onboardingCompleted: boolean("onboarding_completed").default(false).notNull(),
+ onboardingDismissed: boolean("onboarding_dismissed").default(false).notNull(),
+ onboardingAgentRan: boolean("onboarding_agent_ran").default(false).notNull(),
+ onboardingAgentStartedAt: timestamp("onboarding_agent_started_at"),
+ workosOrgId: text("workos_org_id").unique(),
+});
export const members = pgTable(
"members",
@@ -289,8 +297,6 @@ export const members = pgTable(
(table) => [
index("members_organizationId_idx").on(table.organizationId),
index("members_userId_idx").on(table.userId),
- // Serves the membership lookup that runs on every authenticated request and
- // enforces one membership row per (organization, user).
uniqueIndex("members_organizationId_userId_uidx").on(
table.organizationId,
table.userId
@@ -319,7 +325,7 @@ export const githubAppInstallations = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -361,7 +367,7 @@ export const githubIntegrations = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -396,7 +402,7 @@ export const linearIntegrations = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -434,7 +440,7 @@ export const slackIntegrations = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -461,7 +467,7 @@ export const granolaIntegrations = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -514,7 +520,7 @@ export const mcpServerIntegrations = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -602,7 +608,7 @@ export const mcpOAuthCredentials = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -655,7 +661,7 @@ export const mcpOAuthPendingAuthorizations = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -715,7 +721,7 @@ export const mcpToolIndex = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -810,7 +816,7 @@ export const contentTriggers = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -874,7 +880,7 @@ export const brandSettings = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -992,7 +998,7 @@ export const brandReferences = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1018,7 +1024,7 @@ export const brandGuidelines = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1045,7 +1051,7 @@ export const brandGuidelineColors = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1074,7 +1080,7 @@ export const brandGuidelineFonts = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1102,7 +1108,7 @@ export const brandGuidelineTokens = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1136,7 +1142,7 @@ export const brandGuidelineAssets = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1173,7 +1179,7 @@ export const brandGuidelineScreenshots = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1206,7 +1212,7 @@ export const brandSitemaps = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1239,7 +1245,7 @@ export const brandSitemapPages = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1248,7 +1254,6 @@ export const brandSitemapPages = pgTable(
table.sitemapId,
table.category
),
- // The gaps program takes the top pages by word count per crawled sitemap.
index("brandSitemapPages_sitemap_category_wordCount_idx").on(
table.sitemapId,
table.category,
@@ -1278,7 +1283,7 @@ export const connectedSocialAccounts = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1310,7 +1315,7 @@ export const trackedSocialAccounts = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1344,7 +1349,7 @@ export const organizationNotificationSettings = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1369,7 +1374,7 @@ export const projects = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1414,7 +1419,7 @@ export const agentFeedback = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1462,15 +1467,11 @@ export const geoSettings = pgTable(
.notNull()
.default(sql`ARRAY[]::text[]`),
languages: text("languages").array(),
- // null = track the default engine set; otherwise a subset of GEO_ENGINES.
engines: text("engines").array(),
- // Pro feature: ask every model host for zero data retention.
enforceZdr: boolean("enforce_zdr").notNull().default(true),
- // Hidden setting: also run every search-capable model without web search.
trackWithoutSearch: boolean("track_without_search")
.notNull()
.default(false),
- // Engines without a ZDR host the user explicitly approved anyway.
nonZdrApprovedEngines: text("non_zdr_approved_engines")
.array()
.notNull()
@@ -1483,7 +1484,6 @@ export const geoSettings = pgTable(
.array()
.notNull()
.default(sql`ARRAY[]::text[]`),
- // Prompt gaps the user ignored; hidden from Content Gaps and the planner.
ignoredGapPromptIds: text("ignored_gap_prompt_ids")
.array()
.notNull()
@@ -1492,16 +1492,13 @@ export const geoSettings = pgTable(
scanIntervalHours: integer("scan_interval_hours").notNull().default(24),
sentimentAttemptedAt: timestamp("sentiment_attempted_at"),
nextScanAt: timestamp("next_scan_at"),
- // Cron-sweep lease: while set and in the future the row is off limits to
- // other sweeps. Kept separate from `next_scan_at` so a retried tick never
- // loses the slot it is scanning for.
scanLeaseUntil: timestamp("scan_lease_until"),
scanStartedAt: timestamp("scan_started_at"),
lastScanAt: timestamp("last_scan_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1530,14 +1527,12 @@ export const geoPrompts = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
index("geoPrompts_organizationId_idx").on(table.organizationId),
index("geoPrompts_projectId_idx").on(table.projectId),
- // The prompt list and the gaps program both read a project's prompts in
- // `created_at desc` order, which currently costs a heap sort.
index("geoPrompts_projectId_createdAt_idx").on(
table.projectId,
table.createdAt.desc()
@@ -1564,7 +1559,7 @@ export const geoPromptSequences = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1598,7 +1593,7 @@ export const geoPersonas = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1653,7 +1648,7 @@ export const geoCompetitors = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1707,7 +1702,7 @@ export const geoShelfSources = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1785,8 +1780,6 @@ export const geoMentionChecks = pgTable(
engine: text("engine").notNull(),
promptId: text("prompt_id").notNull(),
sequenceId: text("sequence_id"),
- // Set on turns played by a simulated buyer persona; null for tracked
- // prompts and hand-written conversations.
personaId: text("persona_id").references(() => geoPersonas.id, {
onDelete: "cascade",
}),
@@ -1816,8 +1809,6 @@ export const geoMentionChecks = pgTable(
promptTokens: integer("prompt_tokens"),
outputTokens: integer("output_tokens"),
reasoningTokens: integer("reasoning_tokens"),
- // Whether the engine call ran with zero data retention enforced. Null on
- // rows written before the column existed or when the route did not say.
zdrEnforced: boolean("zdr_enforced"),
durationMs: integer("duration_ms"),
costUsd: real("cost_usd"),
@@ -1844,11 +1835,6 @@ export const geoMentionChecks = pgTable(
table.promptId,
table.capturedAt
),
- // Covering index for the GEO analytics window: the aggregate procedures
- // (overview, timeseries, competitor/language share) read only these columns,
- // while the table averages ~900 B/row because of answer/grounding/excerpt.
- // drizzle-orm 0.45 has no `INCLUDE` support, so the payload columns are
- // trailing key columns instead of index-only payload.
index("geoMentionChecks_project_captured_cover_idx").on(
table.projectId,
table.capturedAt,
@@ -1862,9 +1848,6 @@ export const geoMentionChecks = pgTable(
table.sentiment,
table.sequenceId
),
- // Matches the `distinct on (prompt_id, engine) ... order by captured_at desc`
- // shape used by promptResultSummaries/promptResults/competitorDetail/gaps;
- // the existing projectEnginePrompt index has the leading columns swapped.
index("geoMentionChecks_project_prompt_engine_captured_idx").on(
table.projectId,
table.promptId,
@@ -1872,8 +1855,6 @@ export const geoMentionChecks = pgTable(
table.capturedAt.desc()
),
index("geoMentionChecks_competitors_idx").using("gin", table.competitors),
- // sequenceResults orders by exactly this tuple; `sequence_id` appears in no
- // other index.
index("geoMentionChecks_sequence_turn_engine_captured_idx")
.on(table.sequenceId, table.turn, table.engine, table.capturedAt.desc())
.where(sql`${table.sequenceId} IS NOT NULL`),
@@ -1948,7 +1929,7 @@ export const geoAgentReadinessReports = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -1994,7 +1975,7 @@ export const googleSearchConsoleIntegrations = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2040,7 +2021,7 @@ export const geoPromptSuggestions = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2099,7 +2080,7 @@ export const geoContentBriefs = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2111,9 +2092,6 @@ export const geoContentBriefs = pgTable(
table.projectId,
table.status
),
- // Serves the `distinct on (source_kind, source_id) ... order by updated_at
- // desc` read in the gaps program; the partial unique index below excludes
- // published/archived briefs and so cannot serve it.
index("geoContentBriefs_project_source_updated_idx").on(
table.projectId,
table.sourceKind,
@@ -2148,7 +2126,7 @@ export const socialExperiments = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2181,7 +2159,7 @@ export const postCollections = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2229,7 +2207,7 @@ export const posts = pgTable(
status: postStatusEnum("status").default("draft").notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2242,18 +2220,14 @@ export const posts = pgTable(
table.id
),
index("posts_collection_id_idx").on(table.collectionId),
- // content.metrics.get aggregates a year of posts by (created_at, status);
- // posts_org_createdAt_id_idx lacks `status` and forces a heap fetch per row.
index("posts_org_createdAt_status_idx").on(
table.organizationId,
table.createdAt,
table.status
),
- // Adoption analytics looks up the org's first published post.
index("posts_org_published_createdAt_idx")
.on(table.organizationId, table.createdAt)
.where(sql`${table.status} = 'published'`),
- // The gaps program reads the newest posts of one content type per org.
index("posts_org_content_type_updated_at_idx").on(
table.organizationId,
table.contentType,
@@ -2288,7 +2262,7 @@ export const contentPublications = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2324,7 +2298,7 @@ export const skills = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2333,11 +2307,6 @@ export const skills = pgTable(
]
);
-/**
- * Prospect-facing GEO reports built in the console and shared via `/r/{shareToken}`.
- * The report body is denormalised into a few columns for listing; the JSON is
- * the source of truth and includes the raw model answers from the scan.
- */
export const geoProspectReports = pgTable(
"geo_prospect_reports",
{
@@ -2348,7 +2317,6 @@ export const geoProspectReports = pgTable(
createdByUserId: text("created_by_user_id").references(() => users.id, {
onDelete: "set null",
}),
- /** Unguessable token used in the public share link. */
shareToken: text("share_token").notNull(),
status: text("status", { enum: GEO_PROSPECT_REPORT_STATUSES })
.notNull()
@@ -2363,7 +2331,7 @@ export const geoProspectReports = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2429,7 +2397,7 @@ export const autonomyMandates = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2467,7 +2435,7 @@ export const autonomySignals = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2518,7 +2486,7 @@ export const autonomyGoals = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2570,7 +2538,7 @@ export const autonomyRuns = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2622,7 +2590,7 @@ export const autonomyTasks = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2671,7 +2639,7 @@ export const autonomyActions = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2741,7 +2709,7 @@ export const autonomyOutbox = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2772,7 +2740,7 @@ export const autonomyClaims = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
@@ -2797,7 +2765,7 @@ export const autonomyControllerLeases = pgTable(
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
- .$onUpdate(() => /* @__PURE__ */ new Date())
+ .$onUpdate(() => new Date())
.notNull(),
},
(table) => [
diff --git a/packages/posthog/src/events.ts b/packages/posthog/src/events.ts
index 3af8c7e66..17762c323 100644
--- a/packages/posthog/src/events.ts
+++ b/packages/posthog/src/events.ts
@@ -7,6 +7,13 @@ export const POSTHOG_EVENTS = {
LOGIN_FAILED: "login_failed",
PASSWORD_RESET_REQUESTED: "password_reset_requested",
PASSWORD_RESET_COMPLETED: "password_reset_completed",
+ MFA_CHALLENGE_REQUIRED: "mfa_challenge_required",
+ MFA_ENROLLMENT_REQUIRED: "mfa_enrollment_required",
+ MFA_VERIFIED: "mfa_verified",
+ MFA_FACTOR_ENROLLED: "mfa_factor_enrolled",
+ MFA_FACTOR_REMOVED: "mfa_factor_removed",
+ MFA_BACKUP_CODE_USED: "mfa_backup_code_used",
+ MFA_BACKUP_CODES_REGENERATED: "mfa_backup_codes_regenerated",
LOGOUT: "logout",
CALLBACK_ROUTED: "callback_routed",
diff --git a/packages/schemas/src/constants/dashboard/auth.ts b/packages/schemas/src/constants/dashboard/auth.ts
new file mode 100644
index 000000000..51ab46153
--- /dev/null
+++ b/packages/schemas/src/constants/dashboard/auth.ts
@@ -0,0 +1,2 @@
+export const TOTP_CODE_LENGTH = 6;
+export const BACKUP_CODE_LENGTH = 8;
diff --git a/packages/schemas/src/schemas/dashboard/auth/mfa.ts b/packages/schemas/src/schemas/dashboard/auth/mfa.ts
new file mode 100644
index 000000000..cfb50cd9e
--- /dev/null
+++ b/packages/schemas/src/schemas/dashboard/auth/mfa.ts
@@ -0,0 +1,80 @@
+import "zod/compile";
+import {
+ BACKUP_CODE_LENGTH,
+ TOTP_CODE_LENGTH,
+} from "@notra/schemas/constants/dashboard/auth";
+import { returnToSchema } from "@notra/schemas/dashboard/auth/return-to";
+import { normalizeBackupCode } from "@notra/schemas/utils/auth";
+// biome-ignore lint/performance/noNamespaceImport: Zod recommended way to import
+import * as z from "zod";
+
+const AUTH_TOKEN_MAX_LENGTH = 4096;
+const WORKOS_ID_MAX_LENGTH = 128;
+const ONE_TIME_CODE_REGEX = new RegExp(`^\\d{${TOTP_CODE_LENGTH}}$`);
+
+export const totpCodeSchema = z
+ .string()
+ .regex(
+ ONE_TIME_CODE_REGEX,
+ `Enter the ${TOTP_CODE_LENGTH}-digit code from your authenticator app`
+ );
+
+const workosIdSchema = (label: string) =>
+ z.string().min(1, `${label} is missing`).max(WORKOS_ID_MAX_LENGTH);
+
+export const verifyMfaCodeInputSchema = z.object({
+ pendingAuthenticationToken: z
+ .string()
+ .min(1, "Sign-in session is missing")
+ .max(AUTH_TOKEN_MAX_LENGTH),
+ authenticationChallengeId: workosIdSchema("Challenge"),
+ code: totpCodeSchema,
+ returnTo: returnToSchema,
+});
+
+const BACKUP_CODE_REGEX = new RegExp(`^[a-z0-9]{${BACKUP_CODE_LENGTH}}$`);
+
+export const backupCodeSchema = z
+ .string()
+ .transform(normalizeBackupCode)
+ .pipe(z.string().regex(BACKUP_CODE_REGEX, "Enter a valid backup code"));
+
+export const redeemBackupCodeInputSchema = z.object({
+ authenticationChallengeId: workosIdSchema("Challenge"),
+ code: backupCodeSchema,
+ returnTo: returnToSchema,
+});
+
+export const secondFactorCodeSchema = z
+ .string()
+ .trim()
+ .refine(
+ (value) =>
+ ONE_TIME_CODE_REGEX.test(value) ||
+ BACKUP_CODE_REGEX.test(normalizeBackupCode(value)),
+ "Enter the code from your authenticator app or a backup code"
+ );
+
+export const verifyTotpEnrollmentInputSchema = z.object({
+ factorId: workosIdSchema("Factor"),
+ authenticationChallengeId: workosIdSchema("Challenge"),
+ code: totpCodeSchema,
+});
+
+export const discardTotpEnrollmentInputSchema = z.object({
+ factorId: workosIdSchema("Factor"),
+});
+
+export const removeAuthFactorInputSchema = z.object({
+ factorId: workosIdSchema("Factor"),
+ confirmationCode: secondFactorCodeSchema,
+});
+
+export const regenerateBackupCodesInputSchema = z.object({
+ confirmationCode: secondFactorCodeSchema,
+});
+
+export const resumeSocialEnrollmentInputSchema = z.object({
+ flowId: z.uuid("Sign-in session is missing"),
+ returnTo: returnToSchema,
+});
diff --git a/packages/schemas/src/schemas/dashboard/auth/workos-error.ts b/packages/schemas/src/schemas/dashboard/auth/workos-error.ts
index 4abb1799b..05a93c8f2 100644
--- a/packages/schemas/src/schemas/dashboard/auth/workos-error.ts
+++ b/packages/schemas/src/schemas/dashboard/auth/workos-error.ts
@@ -12,6 +12,12 @@ export const workosErrorSchema = z.looseObject({
email: z.string().optional(),
pending_authentication_token: z.string().optional(),
organizations: z.array(z.looseObject({ id: z.string() })).optional(),
+ authentication_factors: z
+ .array(z.looseObject({ id: z.string(), type: z.string() }))
+ .optional(),
+ user: z
+ .looseObject({ id: z.string(), email: z.string().optional() })
+ .optional(),
})
.optional(),
});
diff --git a/packages/schemas/src/types/dashboard/auth.ts b/packages/schemas/src/types/dashboard/auth.ts
new file mode 100644
index 000000000..1f05d563c
--- /dev/null
+++ b/packages/schemas/src/types/dashboard/auth.ts
@@ -0,0 +1,137 @@
+import type * as z from "zod";
+
+import type {
+ forgotPasswordInputSchema,
+ resetPasswordInputSchema,
+ signInWithPasswordInputSchema,
+ signUpWithPasswordInputSchema,
+ verifyEmailCodeInputSchema,
+} from "../../schemas/dashboard/auth/credentials.js";
+import type {
+ discardTotpEnrollmentInputSchema,
+ redeemBackupCodeInputSchema,
+ regenerateBackupCodesInputSchema,
+ removeAuthFactorInputSchema,
+ resumeSocialEnrollmentInputSchema,
+ verifyMfaCodeInputSchema,
+ verifyTotpEnrollmentInputSchema,
+} from "../../schemas/dashboard/auth/mfa.js";
+import type { startSocialSignInInputSchema } from "../../schemas/dashboard/auth/social.js";
+
+export type SignInWithPasswordInput = z.input<
+ typeof signInWithPasswordInputSchema
+>;
+export type SignUpWithPasswordInput = z.input<
+ typeof signUpWithPasswordInputSchema
+>;
+export type VerifyEmailCodeInput = z.input;
+export type ForgotPasswordInput = z.input;
+export type ResetPasswordInput = z.input;
+export type VerifyMfaCodeInput = z.input;
+export type RedeemBackupCodeInput = z.input;
+export type StartSocialSignInInput = z.input<
+ typeof startSocialSignInInputSchema
+>;
+export type VerifyTotpEnrollmentInput = z.input<
+ typeof verifyTotpEnrollmentInputSchema
+>;
+export type DiscardTotpEnrollmentInput = z.input<
+ typeof discardTotpEnrollmentInputSchema
+>;
+export type RemoveAuthFactorInput = z.input;
+export type RegenerateBackupCodesInput = z.input<
+ typeof regenerateBackupCodesInputSchema
+>;
+export type ResumeSocialEnrollmentInput = z.input<
+ typeof resumeSocialEnrollmentInputSchema
+>;
+
+export interface TotpEnrollmentSecrets {
+ qrCode: string;
+ secret: string;
+ otpauthUri: string;
+}
+
+export interface TotpEnrollment extends TotpEnrollmentSecrets {
+ factorId: string;
+ authenticationChallengeId: string;
+}
+
+export interface TotpFactorSummary {
+ id: string;
+ issuer: string | null;
+ createdAt: string;
+}
+
+export interface AuthFlowSuccess {
+ status: "success";
+ redirectTo: string;
+}
+
+export interface AuthFlowEnrolled {
+ status: "enrolled";
+ redirectTo: string;
+ backupCodes: string[];
+}
+
+export interface AuthFlowVerificationRequired {
+ status: "verification-required";
+ pendingAuthenticationToken: string;
+ email: string;
+}
+
+export interface AuthFlowMfaRequired {
+ status: "mfa-required";
+ pendingAuthenticationToken: string;
+ authenticationChallengeId: string;
+ email: string;
+}
+
+export interface AuthFlowMfaEnrollmentRequired extends TotpEnrollment {
+ status: "mfa-enrollment-required";
+ pendingAuthenticationToken: string;
+ email: string;
+}
+
+export interface AuthFlowError {
+ status: "error";
+ message: string;
+}
+
+export type AuthFlowResult =
+ | AuthFlowSuccess
+ | AuthFlowEnrolled
+ | AuthFlowVerificationRequired
+ | AuthFlowMfaRequired
+ | AuthFlowMfaEnrollmentRequired
+ | AuthFlowError;
+
+export type PendingAuthStep =
+ | AuthFlowVerificationRequired
+ | AuthFlowMfaRequired
+ | AuthFlowMfaEnrollmentRequired;
+
+export interface AuthFlowRecovered {
+ status: "recovered";
+ email: string;
+}
+
+export type RedeemBackupCodeResult = AuthFlowRecovered | AuthFlowError;
+
+export interface SecurityOverview {
+ email: string;
+ totpFactors: TotpFactorSummary[];
+ backupCodesRemaining: number;
+}
+
+export type StartTotpEnrollmentResult = TotpEnrollment;
+
+export interface VerifyTotpEnrollmentResult {
+ verified: true;
+ backupCodes: string[] | null;
+ warning: string | null;
+}
+
+export interface RegenerateBackupCodesResult {
+ codes: string[];
+}
diff --git a/packages/schemas/src/utils/auth.ts b/packages/schemas/src/utils/auth.ts
new file mode 100644
index 000000000..e3ff3449f
--- /dev/null
+++ b/packages/schemas/src/utils/auth.ts
@@ -0,0 +1,5 @@
+const BACKUP_CODE_SEPARATOR_REGEX = /[\s-]/g;
+
+export function normalizeBackupCode(code: string): string {
+ return code.toLowerCase().replace(BACKUP_CODE_SEPARATOR_REGEX, "");
+}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 11336373b..febc99d96 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -29,6 +29,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@hugeicons/core-free-icons": "^4.1.1",
"@hugeicons/react": "^1.1.4",
+ "@notra/schemas": "workspace:*",
"@shadcn/react": "^0.2.0",
"@shikijs/transformers": "^3.21.0",
"@tailwindcss/typography": "^0.5.19",
diff --git a/packages/ui/src/components/shared/auth/auth-email-field.tsx b/packages/ui/src/components/shared/auth/auth-email-field.tsx
index 0d94b19a4..160ff2b6f 100644
--- a/packages/ui/src/components/shared/auth/auth-email-field.tsx
+++ b/packages/ui/src/components/shared/auth/auth-email-field.tsx
@@ -1,6 +1,6 @@
"use client";
-import type { AuthEmailFieldProps } from "../../../lib/auth-types";
+import type { AuthEmailFieldProps } from "../../../types/auth";
import { Input } from "../../ui/input";
import { Label } from "../../ui/label";
import { AuthFieldError } from "./auth-field-error";
diff --git a/packages/ui/src/components/shared/auth/auth-field-error.tsx b/packages/ui/src/components/shared/auth/auth-field-error.tsx
index fa85df74d..0154ba7fd 100644
--- a/packages/ui/src/components/shared/auth/auth-field-error.tsx
+++ b/packages/ui/src/components/shared/auth/auth-field-error.tsx
@@ -8,7 +8,7 @@ import {
useReducedMotion,
} from "motion/react";
import { TRANSITION } from "@notra/ui/lib/motion";
-import type { AuthFieldErrorProps } from "../../../lib/auth-types";
+import type { AuthFieldErrorProps } from "../../../types/auth";
export function AuthFieldError({ id, error }: AuthFieldErrorProps) {
const reduceMotion = useReducedMotion();
diff --git a/packages/ui/src/components/shared/auth/auth-form-error.tsx b/packages/ui/src/components/shared/auth/auth-form-error.tsx
index 928019fe9..49f2abc2b 100644
--- a/packages/ui/src/components/shared/auth/auth-form-error.tsx
+++ b/packages/ui/src/components/shared/auth/auth-form-error.tsx
@@ -11,7 +11,7 @@ import {
useReducedMotion,
} from "motion/react";
import { TRANSITION } from "@notra/ui/lib/motion";
-import type { AuthFormErrorProps } from "../../../lib/auth-types";
+import type { AuthFormErrorProps } from "../../../types/auth";
export function AuthFormError({ error, className }: AuthFormErrorProps) {
const reduceMotion = useReducedMotion();
diff --git a/packages/ui/src/components/shared/auth/auth-form-header.tsx b/packages/ui/src/components/shared/auth/auth-form-header.tsx
index c49addbc7..8a0b6bb17 100644
--- a/packages/ui/src/components/shared/auth/auth-form-header.tsx
+++ b/packages/ui/src/components/shared/auth/auth-form-header.tsx
@@ -1,4 +1,4 @@
-import type { AuthFormHeaderProps } from "../../../lib/auth-types";
+import type { AuthFormHeaderProps } from "../../../types/auth";
export function AuthFormHeader({ title, description }: AuthFormHeaderProps) {
if (!(title || description)) {
diff --git a/packages/ui/src/components/shared/auth/auth-password-field.tsx b/packages/ui/src/components/shared/auth/auth-password-field.tsx
index beaa4df76..b966390b9 100644
--- a/packages/ui/src/components/shared/auth/auth-password-field.tsx
+++ b/packages/ui/src/components/shared/auth/auth-password-field.tsx
@@ -3,7 +3,7 @@
import { ViewIcon, ViewOffSlashIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { useState } from "react";
-import type { AuthPasswordFieldProps } from "../../../lib/auth-types";
+import type { AuthPasswordFieldProps } from "../../../types/auth";
import { Input } from "../../ui/input";
import { Label } from "../../ui/label";
import { AuthFieldError } from "./auth-field-error";
diff --git a/packages/ui/src/components/shared/auth/auth-pending-step.tsx b/packages/ui/src/components/shared/auth/auth-pending-step.tsx
new file mode 100644
index 000000000..e160fbfbf
--- /dev/null
+++ b/packages/ui/src/components/shared/auth/auth-pending-step.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import type { AuthPendingStepProps } from "../../../types/auth";
+import { EmailVerificationForm } from "./email-verification-form";
+import { MfaChallengeForm } from "./mfa-challenge-form";
+import { MfaEnrollmentForm } from "./mfa-enrollment-form";
+
+export function AuthPendingStep({
+ step,
+ returnTo,
+ onResult,
+ onFinish,
+ onBack,
+ onRecovered,
+ verifyEmailCode,
+ verifyMfaCode,
+ redeemBackupCode,
+}: AuthPendingStepProps) {
+ switch (step.status) {
+ case "verification-required":
+ return (
+
+ );
+ case "mfa-required":
+ return (
+
+ );
+ case "mfa-enrollment-required":
+ return (
+
+ );
+ default: {
+ const exhaustive: never = step;
+ return exhaustive;
+ }
+ }
+}
diff --git a/packages/ui/src/components/shared/auth/auth-social-buttons.tsx b/packages/ui/src/components/shared/auth/auth-social-buttons.tsx
index 29fedfdf5..6b8898f7c 100644
--- a/packages/ui/src/components/shared/auth/auth-social-buttons.tsx
+++ b/packages/ui/src/components/shared/auth/auth-social-buttons.tsx
@@ -4,7 +4,7 @@ import { Loader2Icon } from "lucide-react";
import type {
AuthSocialButtonsProps,
SocialProvider,
-} from "../../../lib/auth-types";
+} from "../../../types/auth";
import { Badge } from "../../ui/badge";
import { Github } from "../../ui/svgs/github";
import { Google } from "../../ui/svgs/google";
diff --git a/packages/ui/src/components/shared/auth/email-verification-form.tsx b/packages/ui/src/components/shared/auth/email-verification-form.tsx
index 258f67a10..f1b7330d0 100644
--- a/packages/ui/src/components/shared/auth/email-verification-form.tsx
+++ b/packages/ui/src/components/shared/auth/email-verification-form.tsx
@@ -1,21 +1,19 @@
"use client";
+import { TOTP_CODE_LENGTH } from "@notra/schemas/constants/dashboard/auth";
import { Loader2Icon } from "lucide-react";
import { useRef, useState } from "react";
-import type { EmailVerificationFormProps } from "../../../lib/auth-types";
-import { Input } from "../../ui/input";
-import { Label } from "../../ui/label";
+import type { EmailVerificationFormProps } from "../../../types/auth";
import { CtaButton } from "../cta-button";
-import { AuthFormError } from "./auth-form-error";
import { AuthFormHeader } from "./auth-form-header";
+import { TotpCodeInput } from "./totp-code-input";
-const NON_DIGIT_REGEX = /\D/g;
+const VERIFY_ERROR_FALLBACK = "Verification failed. Please try again.";
export function EmailVerificationForm({
- pendingAuthenticationToken,
- email,
+ step,
returnTo,
- onSuccess,
+ onResult,
verifyEmailCode,
}: EmailVerificationFormProps) {
const [code, setCode] = useState("");
@@ -23,81 +21,65 @@ export function EmailVerificationForm({
const [isPending, setIsPending] = useState(false);
const requestIdRef = useRef(0);
- async function handleVerify() {
+ async function handleVerify(submittedCode: string) {
+ if (submittedCode.length !== TOTP_CODE_LENGTH || isPending) {
+ return;
+ }
const requestId = requestIdRef.current + 1;
requestIdRef.current = requestId;
setFormError(null);
setIsPending(true);
const result = await verifyEmailCode({
- pendingAuthenticationToken,
- code,
+ pendingAuthenticationToken: step.pendingAuthenticationToken,
+ code: submittedCode,
returnTo,
}).catch(() => null);
- if (result?.status === "success") {
- if (requestIdRef.current === requestId) {
- if (onSuccess) {
- onSuccess();
- } else {
- window.location.assign(result.redirectTo);
- }
- }
+ if (requestIdRef.current !== requestId) {
return;
}
-
- const nextError =
- result?.status === "error"
- ? result.message
- : "Verification failed. Please try again.";
-
- setFormError((previous) =>
- requestIdRef.current === requestId ? nextError : previous
- );
- setIsPending((previous) =>
- requestIdRef.current === requestId ? false : previous
+ if (result && onResult(result)) {
+ return;
+ }
+ setFormError(
+ result?.status === "error" ? result.message : VERIFY_ERROR_FALLBACK
);
+ setCode("");
+ setIsPending(false);
}
return (