diff --git a/apps/web/app/(auth)/sign-in/page.tsx b/apps/web/app/(auth)/sign-in/page.tsx
index 83b41f6..8300731 100644
--- a/apps/web/app/(auth)/sign-in/page.tsx
+++ b/apps/web/app/(auth)/sign-in/page.tsx
@@ -4,7 +4,7 @@ import { Suspense, useState } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { signIn } from "next-auth/react";
-import { Loader2, Lock, Mail, Sparkles } from "lucide-react";
+import { Loader2, Lock, Mail, ShieldCheck, Sparkles } from "lucide-react";
import { cn } from "@/lib/utils";
function SignInForm() {
@@ -130,47 +130,60 @@ function SignInForm() {
-
+
+
+
+
+
-
diff --git a/apps/web/app/(auth)/sign-up/page.tsx b/apps/web/app/(auth)/sign-up/page.tsx
index 8038cdf..e14bd7d 100644
--- a/apps/web/app/(auth)/sign-up/page.tsx
+++ b/apps/web/app/(auth)/sign-up/page.tsx
@@ -2,7 +2,8 @@
import { useState } from "react";
import Link from "next/link";
-import { Loader2, Mail, Sparkles, User, Lock } from "lucide-react";
+import { signIn } from "next-auth/react";
+import { Loader2, Lock, Mail, ShieldCheck, Sparkles, User } from "lucide-react";
import { cn } from "@/lib/utils";
export default function SignUpPage() {
@@ -134,6 +135,27 @@ export default function SignUpPage() {
+
+
+
+
+
+ Or use single sign-on
+
+
+
+
+
Already have an account?{" "}
Promise;
+
+async function getSql(): Promise {
+ const { db } = await import("@tasks/database/client");
+ return (db as { $client: Sql }).$client;
+}
+
+async function ensureUserIdByEmail(args: {
+ email: string;
+ name?: string | null;
+ image?: string | null;
+}): Promise {
+ const email = args.email.trim();
+ if (!email) return null;
+ const sql = await getSql();
+
+ const existing = (await sql`
+ SELECT id FROM users WHERE lower(email) = lower(${email}) LIMIT 1
+ `) as { id: string }[];
+ if (existing[0]) return existing[0].id;
+
+ await sql`
+ INSERT INTO users (email, name, avatar_url)
+ VALUES (${email.toLowerCase()}, ${args.name ?? null}, ${args.image ?? null})
+ ON CONFLICT (email) DO NOTHING
+ `;
+ const after = (await sql`
+ SELECT id FROM users WHERE lower(email) = lower(${email}) LIMIT 1
+ `) as { id: string }[];
+ return after[0]?.id ?? null;
+}
+
+function slugifyForWorkspace(seed: string): string {
+ const cleaned = seed
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "")
+ .slice(0, 50);
+ return cleaned || "workspace";
+}
+
+/**
+ * Idempotent: if the user already owns or is a member of any workspace, no-op.
+ * Otherwise mint a personal workspace and add them as `owner`. Runs on every
+ * first sign-in (credentials and OAuth alike) so freshly-created OAuth users
+ * don't land in the app with no tenant scope and an unusable session.
+ *
+ * Slug collisions are handled by retrying with a random 6-char suffix; we cap
+ * attempts so a misbehaving DB can't lock the sign-in flow.
+ */
+async function ensureUserHasWorkspace(args: {
+ userId: string;
+ displayName: string | null;
+ email: string;
+}): Promise {
+ const sql = await getSql();
+
+ const existing = (await sql`
+ SELECT 1
+ FROM workspaces w
+ LEFT JOIN workspace_members m
+ ON m.workspace_id = w.id AND m.user_id = ${args.userId}
+ WHERE w.owner_user_id = ${args.userId} OR m.user_id = ${args.userId}
+ LIMIT 1
+ `) as unknown[];
+ if (existing.length > 0) return;
+
+ const trimmedName = args.displayName?.trim() ?? "";
+ const seed = trimmedName || args.email.split("@")[0] || "workspace";
+ const baseSlug = slugifyForWorkspace(seed);
+ const workspaceName = trimmedName ? `${trimmedName}'s workspace` : "My workspace";
+
+ let workspaceId: string | null = null;
+ let candidate = baseSlug;
+ for (let attempt = 0; attempt < 5 && !workspaceId; attempt += 1) {
+ const inserted = (await sql`
+ INSERT INTO workspaces (slug, name, owner_user_id)
+ VALUES (${candidate}, ${workspaceName}, ${args.userId})
+ ON CONFLICT (slug) DO NOTHING
+ RETURNING id
+ `) as { id: string }[];
+ if (inserted[0]) {
+ workspaceId = inserted[0].id;
+ break;
+ }
+ candidate = `${baseSlug}-${Math.random().toString(36).slice(2, 8)}`;
+ }
+ if (!workspaceId) {
+ console.warn("[auth] Failed to provision workspace for user", args.userId);
+ return;
+ }
+
+ await sql`
+ INSERT INTO workspace_members (workspace_id, user_id, role)
+ VALUES (${workspaceId}, ${args.userId}, 'owner')
+ ON CONFLICT (workspace_id, user_id) DO NOTHING
+ `;
+}
+
declare module "next-auth" {
interface Session {
user: {
@@ -38,9 +144,7 @@ const providers: NextAuthConfig["providers"] = [
}
if (password !== devPassword) return null;
- const { db } = await import("@tasks/database/client");
- const sql = (db as { $client: (t: TemplateStringsArray, ...v: unknown[]) => Promise })
- .$client;
+ const sql = await getSql();
const rows = (await sql`
SELECT id, email, name, avatar_url AS "avatarUrl"
FROM users
@@ -99,9 +203,38 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
},
providers,
callbacks: {
- async jwt({ token, user }) {
- if (user?.id) {
- token.id = user.id;
+ async signIn({ user, account }) {
+ // OAuth providers must give us an email so we can map to a `users` row.
+ if (account && account.provider !== "credentials" && !user?.email) {
+ return false;
+ }
+ return true;
+ },
+ async jwt({ token, user, account }) {
+ // First call (sign-in): `user` and `account` are present.
+ if (account && user) {
+ let dbId: string | null = null;
+ if (account.provider === "credentials") {
+ // `authorize` already returns a real DB UUID in `user.id`.
+ dbId = user.id ?? null;
+ } else if (user.email) {
+ dbId = await ensureUserIdByEmail({
+ email: user.email,
+ name: user.name ?? null,
+ image: user.image ?? null,
+ });
+ }
+
+ if (dbId) {
+ token.id = dbId;
+ // Make sure every authenticated user has a tenant they can land in.
+ // Cheap idempotent check; only mints a workspace on the first sign-in.
+ await ensureUserHasWorkspace({
+ userId: dbId,
+ displayName: user.name ?? null,
+ email: user.email ?? "",
+ });
+ }
}
return token;
},
diff --git a/packages/database/migrations/0004_medical_blob.sql b/packages/database/migrations/0004_medical_blob.sql
new file mode 100644
index 0000000..38a0c7e
--- /dev/null
+++ b/packages/database/migrations/0004_medical_blob.sql
@@ -0,0 +1,18 @@
+-- ============================================================================
+-- 0004 — Case-insensitive uniqueness on users.email.
+-- ============================================================================
+-- Belt-and-braces: keep the existing column-level UNIQUE on `email` and add a
+-- UNIQUE expression index on `lower(email)`. This makes the case-insensitive
+-- lookups in `apps/web/lib/auth.ts` (`ensureUserIdByEmail`, the credentials
+-- `authorize`) safe forever, and prevents OAuth providers from minting two
+-- rows that differ only in casing.
+--
+-- We normalize existing rows to lowercase first. If the data already contains
+-- two rows whose emails differ only in case, the UPDATE will hit the existing
+-- column-level UNIQUE and fail loudly — that's the right behavior, since
+-- merging duplicate human accounts requires a human decision.
+-- ============================================================================
+
+UPDATE "users" SET "email" = lower("email") WHERE "email" <> lower("email");
+--> statement-breakpoint
+CREATE UNIQUE INDEX "users_email_lower_unique" ON "users" USING btree (lower("email"));
diff --git a/packages/database/migrations/meta/0004_snapshot.json b/packages/database/migrations/meta/0004_snapshot.json
new file mode 100644
index 0000000..4b3052e
--- /dev/null
+++ b/packages/database/migrations/meta/0004_snapshot.json
@@ -0,0 +1,2429 @@
+{
+ "id": "9492121e-cee1-4628-937b-3d9d325e700a",
+ "prevId": "1dc79026-77bc-459d-a448-b4a42bb815c0",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.workspaces": {
+ "name": "workspaces",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(60)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner_user_id": {
+ "name": "owner_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plan_tier": {
+ "name": "plan_tier",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'free'"
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspaces_slug_unique": {
+ "name": "workspaces_slug_unique",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspaces_owner_user_id_idx": {
+ "name": "workspaces_owner_user_id_idx",
+ "columns": [
+ {
+ "expression": "owner_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspaces_owner_user_id_users_id_fk": {
+ "name": "workspaces_owner_user_id_users_id_fk",
+ "tableFrom": "workspaces",
+ "tableTo": "users",
+ "columnsFrom": [
+ "owner_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.object_assignees": {
+ "name": "object_assignees",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "object_id": {
+ "name": "object_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'assignee'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "object_assignees_object_id_user_id_unique": {
+ "name": "object_assignees_object_id_user_id_unique",
+ "columns": [
+ {
+ "expression": "object_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "object_assignees_object_id_idx": {
+ "name": "object_assignees_object_id_idx",
+ "columns": [
+ {
+ "expression": "object_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "object_assignees_user_id_idx": {
+ "name": "object_assignees_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "object_assignees_object_id_objects_id_fk": {
+ "name": "object_assignees_object_id_objects_id_fk",
+ "tableFrom": "object_assignees",
+ "tableTo": "objects",
+ "columnsFrom": [
+ "object_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "object_assignees_user_id_users_id_fk": {
+ "name": "object_assignees_user_id_users_id_fk",
+ "tableFrom": "object_assignees",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.objects": {
+ "name": "objects",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_image": {
+ "name": "cover_image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content": {
+ "name": "content",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "objects_parent_id_idx": {
+ "name": "objects_parent_id_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "objects_type_idx": {
+ "name": "objects_type_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "objects_workspace_id_idx": {
+ "name": "objects_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "objects_template_id_idx": {
+ "name": "objects_template_id_idx",
+ "columns": [
+ {
+ "expression": "template_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "objects_created_by_idx": {
+ "name": "objects_created_by_idx",
+ "columns": [
+ {
+ "expression": "created_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "objects_type_workspace_id_idx": {
+ "name": "objects_type_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "objects_workspace_id_workspaces_id_fk": {
+ "name": "objects_workspace_id_workspaces_id_fk",
+ "tableFrom": "objects",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "objects_created_by_users_id_fk": {
+ "name": "objects_created_by_users_id_fk",
+ "tableFrom": "objects",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "objects_parent_id_objects_id_fk": {
+ "name": "objects_parent_id_objects_id_fk",
+ "tableFrom": "objects",
+ "tableTo": "objects",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "objects_template_id_templates_id_fk": {
+ "name": "objects_template_id_templates_id_fk",
+ "tableFrom": "objects",
+ "tableTo": "templates",
+ "columnsFrom": [
+ "template_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_members": {
+ "name": "workspace_members",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_members_workspace_id_user_id_unique": {
+ "name": "workspace_members_workspace_id_user_id_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_members_workspace_id_idx": {
+ "name": "workspace_members_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "workspace_members_user_id_idx": {
+ "name": "workspace_members_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_members_workspace_id_workspaces_id_fk": {
+ "name": "workspace_members_workspace_id_workspaces_id_fk",
+ "tableFrom": "workspace_members",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_members_user_id_users_id_fk": {
+ "name": "workspace_members_user_id_users_id_fk",
+ "tableFrom": "workspace_members",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.object_type_defs": {
+ "name": "object_type_defs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "layout": {
+ "name": "layout",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'task'"
+ },
+ "default_properties": {
+ "name": "default_properties",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "object_type_defs_workspace_id_idx": {
+ "name": "object_type_defs_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "object_type_defs_slug_idx": {
+ "name": "object_type_defs_slug_idx",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "object_type_defs_workspace_id_workspaces_id_fk": {
+ "name": "object_type_defs_workspace_id_workspaces_id_fk",
+ "tableFrom": "object_type_defs",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.property_definitions": {
+ "name": "property_definitions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "field_type": {
+ "name": "field_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "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 with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "property_definitions_workspace_id_idx": {
+ "name": "property_definitions_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "property_definitions_workspace_id_name_idx": {
+ "name": "property_definitions_workspace_id_name_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "property_definitions_workspace_id_workspaces_id_fk": {
+ "name": "property_definitions_workspace_id_workspaces_id_fk",
+ "tableFrom": "property_definitions",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.property_values": {
+ "name": "property_values",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "object_id": {
+ "name": "object_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "property_def_id": {
+ "name": "property_def_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "property_values_object_id_property_def_id_unique": {
+ "name": "property_values_object_id_property_def_id_unique",
+ "columns": [
+ {
+ "expression": "object_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "property_def_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "property_values_object_id_idx": {
+ "name": "property_values_object_id_idx",
+ "columns": [
+ {
+ "expression": "object_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "property_values_property_def_id_idx": {
+ "name": "property_values_property_def_id_idx",
+ "columns": [
+ {
+ "expression": "property_def_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "property_values_object_id_objects_id_fk": {
+ "name": "property_values_object_id_objects_id_fk",
+ "tableFrom": "property_values",
+ "tableTo": "objects",
+ "columnsFrom": [
+ "object_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "property_values_property_def_id_property_definitions_id_fk": {
+ "name": "property_values_property_def_id_property_definitions_id_fk",
+ "tableFrom": "property_values",
+ "tableTo": "property_definitions",
+ "columnsFrom": [
+ "property_def_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.views": {
+ "name": "views",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "object_id": {
+ "name": "object_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "view_type": {
+ "name": "view_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "views_object_id_idx": {
+ "name": "views_object_id_idx",
+ "columns": [
+ {
+ "expression": "object_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "views_object_id_objects_id_fk": {
+ "name": "views_object_id_objects_id_fk",
+ "tableFrom": "views",
+ "tableTo": "objects",
+ "columnsFrom": [
+ "object_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.accounts": {
+ "name": "accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_account_id": {
+ "name": "provider_account_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "accounts_provider_provider_account_id_unique": {
+ "name": "accounts_provider_provider_account_id_unique",
+ "columns": [
+ {
+ "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": {}
+ },
+ "accounts_user_id_idx": {
+ "name": "accounts_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "accounts_user_id_users_id_fk": {
+ "name": "accounts_user_id_users_id_fk",
+ "tableFrom": "accounts",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sessions": {
+ "name": "sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "session_token": {
+ "name": "session_token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "sessions_user_id_idx": {
+ "name": "sessions_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "sessions_user_id_users_id_fk": {
+ "name": "sessions_user_id_users_id_fk",
+ "tableFrom": "sessions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "sessions_session_token_unique": {
+ "name": "sessions_session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "session_token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar_url": {
+ "name": "avatar_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "users_email_idx": {
+ "name": "users_email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_email_lower_unique": {
+ "name": "users_email_lower_unique",
+ "columns": [
+ {
+ "expression": "lower(\"email\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verification_tokens": {
+ "name": "verification_tokens",
+ "schema": "",
+ "columns": {
+ "identifier": {
+ "name": "identifier",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verification_tokens_identifier_token_pk": {
+ "name": "verification_tokens_identifier_token_pk",
+ "columns": [
+ "identifier",
+ "token"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.object_relations": {
+ "name": "object_relations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_id": {
+ "name": "target_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "relation_type": {
+ "name": "relation_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "object_relations_source_id_idx": {
+ "name": "object_relations_source_id_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "object_relations_target_id_idx": {
+ "name": "object_relations_target_id_idx",
+ "columns": [
+ {
+ "expression": "target_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "object_relations_relation_type_idx": {
+ "name": "object_relations_relation_type_idx",
+ "columns": [
+ {
+ "expression": "relation_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "object_relations_source_target_type_unique": {
+ "name": "object_relations_source_target_type_unique",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "relation_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "object_relations_source_id_objects_id_fk": {
+ "name": "object_relations_source_id_objects_id_fk",
+ "tableFrom": "object_relations",
+ "tableTo": "objects",
+ "columnsFrom": [
+ "source_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "object_relations_target_id_objects_id_fk": {
+ "name": "object_relations_target_id_objects_id_fk",
+ "tableFrom": "object_relations",
+ "tableTo": "objects",
+ "columnsFrom": [
+ "target_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.templates": {
+ "name": "templates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_type": {
+ "name": "target_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schema": {
+ "name": "schema",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "templates_workspace_id_idx": {
+ "name": "templates_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "templates_workspace_id_workspaces_id_fk": {
+ "name": "templates_workspace_id_workspaces_id_fk",
+ "tableFrom": "templates",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.form_responses": {
+ "name": "form_responses",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "form_id": {
+ "name": "form_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "respondent_id": {
+ "name": "respondent_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_object_id": {
+ "name": "created_object_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "submitted_at": {
+ "name": "submitted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "form_responses_form_id_idx": {
+ "name": "form_responses_form_id_idx",
+ "columns": [
+ {
+ "expression": "form_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "form_responses_respondent_id_idx": {
+ "name": "form_responses_respondent_id_idx",
+ "columns": [
+ {
+ "expression": "respondent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "form_responses_form_id_forms_id_fk": {
+ "name": "form_responses_form_id_forms_id_fk",
+ "tableFrom": "form_responses",
+ "tableTo": "forms",
+ "columnsFrom": [
+ "form_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "form_responses_respondent_id_users_id_fk": {
+ "name": "form_responses_respondent_id_users_id_fk",
+ "tableFrom": "form_responses",
+ "tableTo": "users",
+ "columnsFrom": [
+ "respondent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "form_responses_created_object_id_objects_id_fk": {
+ "name": "form_responses_created_object_id_objects_id_fk",
+ "tableFrom": "form_responses",
+ "tableTo": "objects",
+ "columnsFrom": [
+ "created_object_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.forms": {
+ "name": "forms",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_image": {
+ "name": "cover_image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "object_id": {
+ "name": "object_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_type": {
+ "name": "target_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'task'"
+ },
+ "fields": {
+ "name": "fields",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'"
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "is_published": {
+ "name": "is_published",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "forms_workspace_id_idx": {
+ "name": "forms_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "forms_object_id_idx": {
+ "name": "forms_object_id_idx",
+ "columns": [
+ {
+ "expression": "object_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "forms_workspace_id_workspaces_id_fk": {
+ "name": "forms_workspace_id_workspaces_id_fk",
+ "tableFrom": "forms",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "forms_object_id_objects_id_fk": {
+ "name": "forms_object_id_objects_id_fk",
+ "tableFrom": "forms",
+ "tableTo": "objects",
+ "columnsFrom": [
+ "object_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "forms_created_by_users_id_fk": {
+ "name": "forms_created_by_users_id_fk",
+ "tableFrom": "forms",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_favorites": {
+ "name": "user_favorites",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "object_id": {
+ "name": "object_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "user_favorites_user_object_idx": {
+ "name": "user_favorites_user_object_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "object_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_favorites_user_id_users_id_fk": {
+ "name": "user_favorites_user_id_users_id_fk",
+ "tableFrom": "user_favorites",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_favorites_object_id_objects_id_fk": {
+ "name": "user_favorites_object_id_objects_id_fk",
+ "tableFrom": "user_favorites",
+ "tableTo": "objects",
+ "columnsFrom": [
+ "object_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.markdown_backlog_items": {
+ "name": "markdown_backlog_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "kind": {
+ "name": "kind",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plan_slug": {
+ "name": "plan_slug",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "epic_slug": {
+ "name": "epic_slug",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "repo_path": {
+ "name": "repo_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "priority": {
+ "name": "priority",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "owner": {
+ "name": "owner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "frontmatter": {
+ "name": "frontmatter",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "body_markdown": {
+ "name": "body_markdown",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "content_hash": {
+ "name": "content_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "markdown_backlog_workspace_repo_path_unique": {
+ "name": "markdown_backlog_workspace_repo_path_unique",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "repo_path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "markdown_backlog_workspace_plan_idx": {
+ "name": "markdown_backlog_workspace_plan_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "plan_slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "markdown_backlog_parent_id_idx": {
+ "name": "markdown_backlog_parent_id_idx",
+ "columns": [
+ {
+ "expression": "parent_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "markdown_backlog_workspace_kind_idx": {
+ "name": "markdown_backlog_workspace_kind_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "kind",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "markdown_backlog_items_workspace_id_workspaces_id_fk": {
+ "name": "markdown_backlog_items_workspace_id_workspaces_id_fk",
+ "tableFrom": "markdown_backlog_items",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "markdown_backlog_items_parent_id_markdown_backlog_items_id_fk": {
+ "name": "markdown_backlog_items_parent_id_markdown_backlog_items_id_fk",
+ "tableFrom": "markdown_backlog_items",
+ "tableTo": "markdown_backlog_items",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.cursor_sync_mappings": {
+ "name": "cursor_sync_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "backlog_item_id": {
+ "name": "backlog_item_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cursor_plan_id": {
+ "name": "cursor_plan_id",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cursor_item_id": {
+ "name": "cursor_item_id",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_pulled_at": {
+ "name": "last_pulled_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_pushed_at": {
+ "name": "last_pushed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_content_hash": {
+ "name": "sync_content_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "cursor_sync_mappings_backlog_item_id_unique": {
+ "name": "cursor_sync_mappings_backlog_item_id_unique",
+ "columns": [
+ {
+ "expression": "backlog_item_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "cursor_sync_mappings_workspace_id_idx": {
+ "name": "cursor_sync_mappings_workspace_id_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "cursor_sync_mappings_workspace_id_workspaces_id_fk": {
+ "name": "cursor_sync_mappings_workspace_id_workspaces_id_fk",
+ "tableFrom": "cursor_sync_mappings",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "cursor_sync_mappings_backlog_item_id_markdown_backlog_items_id_fk": {
+ "name": "cursor_sync_mappings_backlog_item_id_markdown_backlog_items_id_fk",
+ "tableFrom": "cursor_sync_mappings",
+ "tableTo": "markdown_backlog_items",
+ "columnsFrom": [
+ "backlog_item_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json
index 7f41d0b..5b22019 100644
--- a/packages/database/migrations/meta/_journal.json
+++ b/packages/database/migrations/meta/_journal.json
@@ -29,6 +29,13 @@
"when": 1778124738113,
"tag": "0003_damp_green_goblin",
"breakpoints": true
+ },
+ {
+ "idx": 4,
+ "version": "7",
+ "when": 1779987416637,
+ "tag": "0004_medical_blob",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/database/src/schema/users.ts b/packages/database/src/schema/users.ts
index a37af0d..c8eedc5 100644
--- a/packages/database/src/schema/users.ts
+++ b/packages/database/src/schema/users.ts
@@ -1,3 +1,4 @@
+import { sql } from "drizzle-orm";
import {
pgTable,
uuid,
@@ -22,6 +23,12 @@ export const users = pgTable(
},
(table) => ({
emailIdx: index("users_email_idx").on(table.email),
+ // Belt-and-braces: existing column-level UNIQUE on `email` plus a
+ // case-insensitive UNIQUE on `lower(email)`. The latter prevents
+ // accidentally storing `Alice@x.com` and `alice@x.com` as two users
+ // and makes the case-insensitive lookups in `apps/web/lib/auth.ts`
+ // safe even if upstream rows were created mixed-case.
+ emailLowerUnique: uniqueIndex("users_email_lower_unique").on(sql`lower(${table.email})`),
}),
);