From 12299947d205e7967f93c295f515a0e418b54cbd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20De=20Le=C3=B3n?= <deleonalonso77@gmail.com>
Date: Thu, 18 Jun 2026 11:56:56 -0600
Subject: [PATCH 1/2] feat: add GitHub auth and Turso persistence for saved
 graphs and testimonials

- Auth.js v5 with GitHub provider and Drizzle adapter (database sessions)
- Turso/libSQL via Drizzle: users, accounts, sessions, graphs, testimonials
- Save graphs per user (private) with history pages under /[lang]/graphs
- Public testimonial wall on the landing page
- Zod-validated server actions for graphs, testimonials and auth
- Header sign in/out + avatar; save button in the workspace
- i18n keys (auth.*) across all six locales
- CSP: allow GitHub avatars in img-src
- Fix pnpm-workspace.yaml to standard onlyBuiltDependencies format
---
 .env.example                                |   13 +
 .gitignore                                  |    5 +
 drizzle.config.ts                           |   13 +
 package.json                                |   11 +-
 pnpm-lock.yaml                              | 1225 ++++++++++++++++++-
 pnpm-workspace.yaml                         |    6 +-
 src/app/[lang]/app/page.tsx                 |    6 +
 src/app/[lang]/graphs/[id]/page.tsx         |   59 +
 src/app/[lang]/graphs/page.tsx              |   71 ++
 src/app/[lang]/layout.tsx                   |    4 +-
 src/app/[lang]/page.tsx                     |    2 +
 src/app/api/auth/[...nextauth]/route.ts     |    6 +
 src/auth.ts                                 |   28 +
 src/components/layout/Header.tsx            |   47 +-
 src/components/sections/TestimonialForm.tsx |   57 +
 src/components/sections/Testimonials.tsx    |   70 ++
 src/components/ui/CodeWorkspace.tsx         |   56 +-
 src/components/ui/GraphListItem.tsx         |   68 +
 src/db/index.ts                             |   16 +
 src/db/schema.ts                            |   95 ++
 src/i18n/dictionaries/ar.json               |   22 +-
 src/i18n/dictionaries/en.json               |   22 +-
 src/i18n/dictionaries/es.json               |   22 +-
 src/i18n/dictionaries/fr.json               |   22 +-
 src/i18n/dictionaries/it.json               |   22 +-
 src/i18n/dictionaries/pt.json               |   22 +-
 src/lib/auth-actions.ts                     |   11 +
 src/lib/graphs.ts                           |   74 ++
 src/lib/testimonials.ts                     |   42 +
 src/lib/validation.test.ts                  |   69 ++
 src/lib/validation.ts                       |   45 +
 src/proxy.ts                                |    2 +-
 32 files changed, 2206 insertions(+), 27 deletions(-)
 create mode 100644 .env.example
 create mode 100644 drizzle.config.ts
 create mode 100644 src/app/[lang]/graphs/[id]/page.tsx
 create mode 100644 src/app/[lang]/graphs/page.tsx
 create mode 100644 src/app/api/auth/[...nextauth]/route.ts
 create mode 100644 src/auth.ts
 create mode 100644 src/components/sections/TestimonialForm.tsx
 create mode 100644 src/components/sections/Testimonials.tsx
 create mode 100644 src/components/ui/GraphListItem.tsx
 create mode 100644 src/db/index.ts
 create mode 100644 src/db/schema.ts
 create mode 100644 src/lib/auth-actions.ts
 create mode 100644 src/lib/graphs.ts
 create mode 100644 src/lib/testimonials.ts
 create mode 100644 src/lib/validation.test.ts
 create mode 100644 src/lib/validation.ts

diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..48928a1
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,13 @@
+# Auth.js — generate a secret with: npx auth secret
+AUTH_SECRET=
+
+# GitHub OAuth app credentials (Settings → Developer settings → OAuth Apps).
+# Callback URL: http://localhost:3000/api/auth/callback/github (local)
+AUTH_GITHUB_ID=
+AUTH_GITHUB_SECRET=
+
+# Turso / libSQL. Local dev uses a SQLite file; prod uses a libsql:// URL + token.
+# Local:  TURSO_DATABASE_URL=file:local.db   (leave TURSO_AUTH_TOKEN empty)
+# Prod:   TURSO_DATABASE_URL=libsql://<db>.turso.io
+TURSO_DATABASE_URL=file:local.db
+TURSO_AUTH_TOKEN=
diff --git a/.gitignore b/.gitignore
index fa736f8..a522b23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,7 @@ yarn-error.log*
 
 # env files (can opt-in for committing if needed)
 .env*
+!.env.example
 
 # vercel
 .vercel
@@ -38,6 +39,10 @@ yarn-error.log*
 *.tsbuildinfo
 next-env.d.ts
 
+# local database (libSQL/SQLite)
+/local.db
+local.db*
+
 # claude code
 .claude/
 CLAUDE.local.md
diff --git a/drizzle.config.ts b/drizzle.config.ts
new file mode 100644
index 0000000..5721e29
--- /dev/null
+++ b/drizzle.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from "drizzle-kit";
+
+export default defineConfig({
+  dialect: "turso",
+  schema: "./src/db/schema.ts",
+  out: "./drizzle",
+  dbCredentials: {
+    url: process.env.TURSO_DATABASE_URL ?? "file:local.db",
+    // The turso dialect requires a token; libSQL ignores it for file: URLs, so a
+    // placeholder keeps local `db:push` working without a real Turso token.
+    authToken: process.env.TURSO_AUTH_TOKEN || "local",
+  },
+});
diff --git a/package.json b/package.json
index c8a65d9..24529d9 100644
--- a/package.json
+++ b/package.json
@@ -16,30 +16,39 @@
     "lint": "eslint",
     "typecheck": "tsc --noEmit",
     "test": "vitest run",
+    "db:push": "drizzle-kit push",
+    "db:generate": "drizzle-kit generate",
+    "db:studio": "drizzle-kit studio",
     "format": "prettier --write .",
     "format:check": "prettier --check ."
   },
   "dependencies": {
+    "@auth/drizzle-adapter": "^1.11.2",
     "@dagrejs/dagre": "^3.0.0",
     "@fontsource-variable/lexend": "^5.2.11",
     "@gsap/react": "^2.1.2",
+    "@libsql/client": "^0.17.4",
     "@tailwindcss/postcss": "^4.3.1",
     "@vercel/analytics": "^2.0.1",
+    "drizzle-orm": "^0.45.2",
     "gsap": "^3.15.0",
     "html-to-image": "1.11.11",
     "jspdf": "^4.2.1",
     "next": "16.2.9",
+    "next-auth": "5.0.0-beta.31",
     "react": "19.2.4",
     "react-dom": "19.2.4",
     "reactflow": "^11.11.4",
     "tailwindcss": "^4.3.1",
-    "web-tree-sitter": "^0.22.6"
+    "web-tree-sitter": "^0.22.6",
+    "zod": "^4.4.3"
   },
   "devDependencies": {
     "@types/dagre": "^0.7.54",
     "@types/node": "^20",
     "@types/react": "^19",
     "@types/react-dom": "^19",
+    "drizzle-kit": "^0.31.10",
     "eslint": "^9",
     "eslint-config-next": "16.2.9",
     "prettier": "^3.8.4",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c993e7e..b468112 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
 
   .:
     dependencies:
+      '@auth/drizzle-adapter':
+        specifier: ^1.11.2
+        version: 1.11.2
       '@dagrejs/dagre':
         specifier: ^3.0.0
         version: 3.0.0
@@ -17,12 +20,18 @@ importers:
       '@gsap/react':
         specifier: ^2.1.2
         version: 2.1.2(gsap@3.15.0)(react@19.2.4)
+      '@libsql/client':
+        specifier: ^0.17.4
+        version: 0.17.4
       '@tailwindcss/postcss':
         specifier: ^4.3.1
         version: 4.3.1
       '@vercel/analytics':
         specifier: ^2.0.1
         version: 2.0.1(next@16.2.9(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
+      drizzle-orm:
+        specifier: ^0.45.2
+        version: 0.45.2(@libsql/client@0.17.4)
       gsap:
         specifier: ^3.15.0
         version: 3.15.0
@@ -35,6 +44,9 @@ importers:
       next:
         specifier: 16.2.9
         version: 16.2.9(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+      next-auth:
+        specifier: 5.0.0-beta.31
+        version: 5.0.0-beta.31(next@16.2.9(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
       react:
         specifier: 19.2.4
         version: 19.2.4
@@ -50,6 +62,9 @@ importers:
       web-tree-sitter:
         specifier: ^0.22.6
         version: 0.22.6
+      zod:
+        specifier: ^4.4.3
+        version: 4.4.3
     devDependencies:
       '@types/dagre':
         specifier: ^0.7.54
@@ -63,6 +78,9 @@ importers:
       '@types/react-dom':
         specifier: ^19
         version: 19.2.3(@types/react@19.2.17)
+      drizzle-kit:
+        specifier: ^0.31.10
+        version: 0.31.10
       eslint:
         specifier: ^9
         version: 9.39.4(jiti@2.7.0)
@@ -80,7 +98,7 @@ importers:
         version: 5.9.3
       vitest:
         specifier: ^4.1.9
-        version: 4.1.9(@types/node@20.19.43)(vite@8.0.16(@types/node@20.19.43)(jiti@2.7.0))
+        version: 4.1.9(@types/node@20.19.43)(vite@8.0.16(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4))
 
 packages:
 
@@ -88,6 +106,23 @@ packages:
     resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
     engines: {node: '>=10'}
 
+  '@auth/core@0.41.2':
+    resolution: {integrity: sha512-Hx5MNBxN2fJTbJKGUKAA0wca43D0Akl3TvufY54Gn8lop7F+34vU1zA1pn0vQfIoVuLIrpfc2nkyjwIaPJMW7w==}
+    peerDependencies:
+      '@simplewebauthn/browser': ^9.0.1
+      '@simplewebauthn/server': ^9.0.2
+      nodemailer: ^7.0.7
+    peerDependenciesMeta:
+      '@simplewebauthn/browser':
+        optional: true
+      '@simplewebauthn/server':
+        optional: true
+      nodemailer:
+        optional: true
+
+  '@auth/drizzle-adapter@1.11.2':
+    resolution: {integrity: sha512-VOuj7REI8jfJjpSbsYwDM/Zrn55T6lS9Yc+29V+EXMcel8eqG7x+7LodNfd1WHjakfkLYi+qsbYUHB3E6aDA4w==}
+
   '@babel/code-frame@7.29.7':
     resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==}
     engines: {node: '>=6.9.0'}
@@ -165,6 +200,9 @@ packages:
   '@dagrejs/graphlib@4.0.1':
     resolution: {integrity: sha512-IvcV6FduIIAmLwnH+yun+QtV36SC7mERqa86aClNqmMN09WhmPPYU8ckHrZBozErf+UvHPWOTJYaGYiIcs0DgA==}
 
+  '@drizzle-team/brocli@0.10.2':
+    resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
+
   '@emnapi/core@1.10.0':
     resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
 
@@ -177,6 +215,458 @@ packages:
   '@emnapi/wasi-threads@1.2.1':
     resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
 
+  '@esbuild-kit/core-utils@3.3.2':
+    resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
+    deprecated: 'Merged into tsx: https://tsx.is'
+
+  '@esbuild-kit/esm-loader@2.6.5':
+    resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==}
+    deprecated: 'Merged into tsx: https://tsx.is'
+
+  '@esbuild/aix-ppc64@0.25.12':
+    resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/aix-ppc64@0.28.1':
+    resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/android-arm64@0.18.20':
+    resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/android-arm64@0.25.12':
+    resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/android-arm64@0.28.1':
+    resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/android-arm@0.18.20':
+    resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/android-arm@0.25.12':
+    resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/android-arm@0.28.1':
+    resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/android-x64@0.18.20':
+    resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/android-x64@0.25.12':
+    resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/android-x64@0.28.1':
+    resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/darwin-arm64@0.18.20':
+    resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/darwin-arm64@0.25.12':
+    resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/darwin-arm64@0.28.1':
+    resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/darwin-x64@0.18.20':
+    resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/darwin-x64@0.25.12':
+    resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/darwin-x64@0.28.1':
+    resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/freebsd-arm64@0.18.20':
+    resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-arm64@0.25.12':
+    resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-arm64@0.28.1':
+    resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-x64@0.18.20':
+    resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-x64@0.25.12':
+    resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-x64@0.28.1':
+    resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/linux-arm64@0.18.20':
+    resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/linux-arm64@0.25.12':
+    resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/linux-arm64@0.28.1':
+    resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/linux-arm@0.18.20':
+    resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/linux-arm@0.25.12':
+    resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/linux-arm@0.28.1':
+    resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/linux-ia32@0.18.20':
+    resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/linux-ia32@0.25.12':
+    resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/linux-ia32@0.28.1':
+    resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/linux-loong64@0.18.20':
+    resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
+    engines: {node: '>=12'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/linux-loong64@0.25.12':
+    resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/linux-loong64@0.28.1':
+    resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/linux-mips64el@0.18.20':
+    resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
+    engines: {node: '>=12'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/linux-mips64el@0.25.12':
+    resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/linux-mips64el@0.28.1':
+    resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/linux-ppc64@0.18.20':
+    resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/linux-ppc64@0.25.12':
+    resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/linux-ppc64@0.28.1':
+    resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/linux-riscv64@0.18.20':
+    resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
+    engines: {node: '>=12'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/linux-riscv64@0.25.12':
+    resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/linux-riscv64@0.28.1':
+    resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/linux-s390x@0.18.20':
+    resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
+    engines: {node: '>=12'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/linux-s390x@0.25.12':
+    resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/linux-s390x@0.28.1':
+    resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/linux-x64@0.18.20':
+    resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/linux-x64@0.25.12':
+    resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/linux-x64@0.28.1':
+    resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/netbsd-arm64@0.25.12':
+    resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [netbsd]
+
+  '@esbuild/netbsd-arm64@0.28.1':
+    resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [netbsd]
+
+  '@esbuild/netbsd-x64@0.18.20':
+    resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/netbsd-x64@0.25.12':
+    resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/netbsd-x64@0.28.1':
+    resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/openbsd-arm64@0.25.12':
+    resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
+  '@esbuild/openbsd-arm64@0.28.1':
+    resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
+  '@esbuild/openbsd-x64@0.18.20':
+    resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/openbsd-x64@0.25.12':
+    resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/openbsd-x64@0.28.1':
+    resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/openharmony-arm64@0.25.12':
+    resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@esbuild/openharmony-arm64@0.28.1':
+    resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@esbuild/sunos-x64@0.18.20':
+    resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/sunos-x64@0.25.12':
+    resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/sunos-x64@0.28.1':
+    resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/win32-arm64@0.18.20':
+    resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/win32-arm64@0.25.12':
+    resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/win32-arm64@0.28.1':
+    resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/win32-ia32@0.18.20':
+    resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/win32-ia32@0.25.12':
+    resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/win32-ia32@0.28.1':
+    resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/win32-x64@0.18.20':
+    resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [win32]
+
+  '@esbuild/win32-x64@0.25.12':
+    resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
+  '@esbuild/win32-x64@0.28.1':
+    resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
   '@eslint-community/eslint-utils@4.9.1':
     resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -413,12 +903,72 @@ packages:
   '@jridgewell/trace-mapping@0.3.31':
     resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
 
+  '@libsql/client@0.17.4':
+    resolution: {integrity: sha512-lYayFWasDV78A+TjlEhr6ubb3odBV6OHjb+wdp8VQcyWWAEIjuwbCHaraEUS4m4yWoo0BvZo96It4VdzZRmRWw==}
+
+  '@libsql/core@0.17.4':
+    resolution: {integrity: sha512-LqF9gIvnJ38nmAH1y/ChizHqDO/MO1wLgA96XrraulEEbqXxLjleSH92YWTolbuJKgPUmGu4aJk9W3UnAcxLOQ==}
+
+  '@libsql/darwin-arm64@0.5.29':
+    resolution: {integrity: sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@libsql/darwin-x64@0.5.29':
+    resolution: {integrity: sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==}
+    cpu: [x64]
+    os: [darwin]
+
+  '@libsql/hrana-client@0.10.0':
+    resolution: {integrity: sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw==}
+
+  '@libsql/isomorphic-ws@0.1.5':
+    resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==}
+
+  '@libsql/linux-arm-gnueabihf@0.5.29':
+    resolution: {integrity: sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==}
+    cpu: [arm]
+    os: [linux]
+
+  '@libsql/linux-arm-musleabihf@0.5.29':
+    resolution: {integrity: sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==}
+    cpu: [arm]
+    os: [linux]
+
+  '@libsql/linux-arm64-gnu@0.5.29':
+    resolution: {integrity: sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@libsql/linux-arm64-musl@0.5.29':
+    resolution: {integrity: sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@libsql/linux-x64-gnu@0.5.29':
+    resolution: {integrity: sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==}
+    cpu: [x64]
+    os: [linux]
+
+  '@libsql/linux-x64-musl@0.5.29':
+    resolution: {integrity: sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==}
+    cpu: [x64]
+    os: [linux]
+
+  '@libsql/win32-x64-msvc@0.5.29':
+    resolution: {integrity: sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==}
+    cpu: [x64]
+    os: [win32]
+
   '@napi-rs/wasm-runtime@1.1.5':
     resolution: {integrity: sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==}
     peerDependencies:
       '@emnapi/core': ^1.7.1
       '@emnapi/runtime': ^1.7.1
 
+  '@neon-rs/load@0.0.4':
+    resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==}
+
   '@next/env@16.2.9':
     resolution: {integrity: sha512-ki5VxxXfzD/9TDe13wyeTKIjQTAwBVpnr8KhRDUr8ltMUq1/NBpWNT5tiPoxiGl+PHM4X2ahSOiPk6iAimIzPg==}
 
@@ -496,6 +1046,9 @@ packages:
   '@oxc-project/types@0.133.0':
     resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==}
 
+  '@panva/hkdf@1.2.1':
+    resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==}
+
   '@reactflow/background@11.3.14':
     resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
     peerDependencies:
@@ -868,6 +1421,9 @@ packages:
   '@types/trusted-types@2.0.7':
     resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
 
+  '@types/ws@8.18.1':
+    resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
+
   '@typescript-eslint/eslint-plugin@8.61.0':
     resolution: {integrity: sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -1216,6 +1772,9 @@ packages:
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
     hasBin: true
 
+  buffer-from@1.1.2:
+    resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+
   call-bind-apply-helpers@1.0.2:
     resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
     engines: {node: '>= 0.4'}
@@ -1360,6 +1919,10 @@ packages:
     resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
     engines: {node: '>= 0.4'}
 
+  detect-libc@2.0.2:
+    resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
+    engines: {node: '>=8'}
+
   detect-libc@2.1.2:
     resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
     engines: {node: '>=8'}
@@ -1371,6 +1934,102 @@ packages:
   dompurify@3.4.10:
     resolution: {integrity: sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==}
 
+  drizzle-kit@0.31.10:
+    resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==}
+    hasBin: true
+
+  drizzle-orm@0.45.2:
+    resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==}
+    peerDependencies:
+      '@aws-sdk/client-rds-data': '>=3'
+      '@cloudflare/workers-types': '>=4'
+      '@electric-sql/pglite': '>=0.2.0'
+      '@libsql/client': '>=0.10.0'
+      '@libsql/client-wasm': '>=0.10.0'
+      '@neondatabase/serverless': '>=0.10.0'
+      '@op-engineering/op-sqlite': '>=2'
+      '@opentelemetry/api': ^1.4.1
+      '@planetscale/database': '>=1.13'
+      '@prisma/client': '*'
+      '@tidbcloud/serverless': '*'
+      '@types/better-sqlite3': '*'
+      '@types/pg': '*'
+      '@types/sql.js': '*'
+      '@upstash/redis': '>=1.34.7'
+      '@vercel/postgres': '>=0.8.0'
+      '@xata.io/client': '*'
+      better-sqlite3: '>=7'
+      bun-types: '*'
+      expo-sqlite: '>=14.0.0'
+      gel: '>=2'
+      knex: '*'
+      kysely: '*'
+      mysql2: '>=2'
+      pg: '>=8'
+      postgres: '>=3'
+      prisma: '*'
+      sql.js: '>=1'
+      sqlite3: '>=5'
+    peerDependenciesMeta:
+      '@aws-sdk/client-rds-data':
+        optional: true
+      '@cloudflare/workers-types':
+        optional: true
+      '@electric-sql/pglite':
+        optional: true
+      '@libsql/client':
+        optional: true
+      '@libsql/client-wasm':
+        optional: true
+      '@neondatabase/serverless':
+        optional: true
+      '@op-engineering/op-sqlite':
+        optional: true
+      '@opentelemetry/api':
+        optional: true
+      '@planetscale/database':
+        optional: true
+      '@prisma/client':
+        optional: true
+      '@tidbcloud/serverless':
+        optional: true
+      '@types/better-sqlite3':
+        optional: true
+      '@types/pg':
+        optional: true
+      '@types/sql.js':
+        optional: true
+      '@upstash/redis':
+        optional: true
+      '@vercel/postgres':
+        optional: true
+      '@xata.io/client':
+        optional: true
+      better-sqlite3:
+        optional: true
+      bun-types:
+        optional: true
+      expo-sqlite:
+        optional: true
+      gel:
+        optional: true
+      knex:
+        optional: true
+      kysely:
+        optional: true
+      mysql2:
+        optional: true
+      pg:
+        optional: true
+      postgres:
+        optional: true
+      prisma:
+        optional: true
+      sql.js:
+        optional: true
+      sqlite3:
+        optional: true
+
   dunder-proto@1.0.1:
     resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
     engines: {node: '>= 0.4'}
@@ -1416,9 +2075,24 @@ packages:
     resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==}
     engines: {node: '>= 0.4'}
 
-  es-to-primitive@1.3.0:
-    resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
-    engines: {node: '>= 0.4'}
+  es-to-primitive@1.3.0:
+    resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
+    engines: {node: '>= 0.4'}
+
+  esbuild@0.18.20:
+    resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
+    engines: {node: '>=12'}
+    hasBin: true
+
+  esbuild@0.25.12:
+    resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+    engines: {node: '>=18'}
+    hasBin: true
+
+  esbuild@0.28.1:
+    resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
+    engines: {node: '>=18'}
+    hasBin: true
 
   escalade@3.2.0:
     resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
@@ -1861,6 +2535,12 @@ packages:
     resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
     hasBin: true
 
+  jose@6.2.3:
+    resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
+
+  js-base64@3.7.8:
+    resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==}
+
   js-tokens@4.0.0:
     resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
 
@@ -1912,6 +2592,11 @@ packages:
     resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
     engines: {node: '>= 0.8.0'}
 
+  libsql@0.5.29:
+    resolution: {integrity: sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==}
+    cpu: [x64, arm64, wasm32, arm]
+    os: [darwin, linux, win32]
+
   lightningcss-android-arm64@1.32.0:
     resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==}
     engines: {node: '>= 12.0.0'}
@@ -2041,6 +2726,22 @@ packages:
   natural-compare@1.4.0:
     resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
 
+  next-auth@5.0.0-beta.31:
+    resolution: {integrity: sha512-1OBgCKPzo+S7UWWMp3xgvGvIJ0OpV7B3vR4ZDRqD9a4Ch+OT6dakLXG9ivhtmIWVa71nTSXattOHyCg8sNi8/Q==}
+    peerDependencies:
+      '@simplewebauthn/browser': ^9.0.1
+      '@simplewebauthn/server': ^9.0.2
+      next: ^14.0.0-0 || ^15.0.0 || ^16.0.0
+      nodemailer: ^7.0.7
+      react: ^18.2.0 || ^19.0.0
+    peerDependenciesMeta:
+      '@simplewebauthn/browser':
+        optional: true
+      '@simplewebauthn/server':
+        optional: true
+      nodemailer:
+        optional: true
+
   next@16.2.9:
     resolution: {integrity: sha512-MEOJiq/UvuezAdqVSceHbqDgZt1kDw2tpGVOlsdIoJsQdbN2JY2hpVG4xnXGkbdJUOEWhnRfiu/O4Hpc9Juwww==}
     engines: {node: '>=20.9.0'}
@@ -2070,6 +2771,9 @@ packages:
     resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==}
     engines: {node: '>=18'}
 
+  oauth4webapi@3.8.6:
+    resolution: {integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==}
+
   object-assign@4.1.1:
     resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
     engines: {node: '>=0.10.0'}
@@ -2169,6 +2873,14 @@ packages:
     resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
     engines: {node: ^10 || ^12 || >=14}
 
+  preact-render-to-string@6.5.11:
+    resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==}
+    peerDependencies:
+      preact: '>=10'
+
+  preact@10.24.3:
+    resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==}
+
   prelude-ls@1.2.1:
     resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
     engines: {node: '>= 0.8.0'}
@@ -2178,6 +2890,9 @@ packages:
     engines: {node: '>=14'}
     hasBin: true
 
+  promise-limit@2.7.0:
+    resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==}
+
   prop-types@15.8.1:
     resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
 
@@ -2319,6 +3034,13 @@ packages:
     resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
     engines: {node: '>=0.10.0'}
 
+  source-map-support@0.5.21:
+    resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+
+  source-map@0.6.1:
+    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+    engines: {node: '>=0.10.0'}
+
   stable-hash@0.0.5:
     resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
 
@@ -2436,6 +3158,11 @@ packages:
   tslib@2.8.1:
     resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
 
+  tsx@4.22.4:
+    resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==}
+    engines: {node: '>=18.0.0'}
+    hasBin: true
+
   type-check@0.4.0:
     resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
     engines: {node: '>= 0.8.0'}
@@ -2612,6 +3339,18 @@ packages:
     resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
     engines: {node: '>=0.10.0'}
 
+  ws@8.21.0:
+    resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==}
+    engines: {node: '>=10.0.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: '>=5.0.2'
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+
   yallist@3.1.1:
     resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
 
@@ -2647,6 +3386,22 @@ snapshots:
 
   '@alloc/quick-lru@5.2.0': {}
 
+  '@auth/core@0.41.2':
+    dependencies:
+      '@panva/hkdf': 1.2.1
+      jose: 6.2.3
+      oauth4webapi: 3.8.6
+      preact: 10.24.3
+      preact-render-to-string: 6.5.11(preact@10.24.3)
+
+  '@auth/drizzle-adapter@1.11.2':
+    dependencies:
+      '@auth/core': 0.41.2
+    transitivePeerDependencies:
+      - '@simplewebauthn/browser'
+      - '@simplewebauthn/server'
+      - nodemailer
+
   '@babel/code-frame@7.29.7':
     dependencies:
       '@babel/helper-validator-identifier': 7.29.7
@@ -2755,6 +3510,8 @@ snapshots:
 
   '@dagrejs/graphlib@4.0.1': {}
 
+  '@drizzle-team/brocli@0.10.2': {}
+
   '@emnapi/core@1.10.0':
     dependencies:
       '@emnapi/wasi-threads': 1.2.1
@@ -2776,6 +3533,238 @@ snapshots:
       tslib: 2.8.1
     optional: true
 
+  '@esbuild-kit/core-utils@3.3.2':
+    dependencies:
+      esbuild: 0.18.20
+      source-map-support: 0.5.21
+
+  '@esbuild-kit/esm-loader@2.6.5':
+    dependencies:
+      '@esbuild-kit/core-utils': 3.3.2
+      get-tsconfig: 4.14.0
+
+  '@esbuild/aix-ppc64@0.25.12':
+    optional: true
+
+  '@esbuild/aix-ppc64@0.28.1':
+    optional: true
+
+  '@esbuild/android-arm64@0.18.20':
+    optional: true
+
+  '@esbuild/android-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/android-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/android-arm@0.18.20':
+    optional: true
+
+  '@esbuild/android-arm@0.25.12':
+    optional: true
+
+  '@esbuild/android-arm@0.28.1':
+    optional: true
+
+  '@esbuild/android-x64@0.18.20':
+    optional: true
+
+  '@esbuild/android-x64@0.25.12':
+    optional: true
+
+  '@esbuild/android-x64@0.28.1':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.18.20':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/darwin-x64@0.18.20':
+    optional: true
+
+  '@esbuild/darwin-x64@0.25.12':
+    optional: true
+
+  '@esbuild/darwin-x64@0.28.1':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.18.20':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.18.20':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.25.12':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.28.1':
+    optional: true
+
+  '@esbuild/linux-arm64@0.18.20':
+    optional: true
+
+  '@esbuild/linux-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/linux-arm@0.18.20':
+    optional: true
+
+  '@esbuild/linux-arm@0.25.12':
+    optional: true
+
+  '@esbuild/linux-arm@0.28.1':
+    optional: true
+
+  '@esbuild/linux-ia32@0.18.20':
+    optional: true
+
+  '@esbuild/linux-ia32@0.25.12':
+    optional: true
+
+  '@esbuild/linux-ia32@0.28.1':
+    optional: true
+
+  '@esbuild/linux-loong64@0.18.20':
+    optional: true
+
+  '@esbuild/linux-loong64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-loong64@0.28.1':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.18.20':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.25.12':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.28.1':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.18.20':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.28.1':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.18.20':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.28.1':
+    optional: true
+
+  '@esbuild/linux-s390x@0.18.20':
+    optional: true
+
+  '@esbuild/linux-s390x@0.25.12':
+    optional: true
+
+  '@esbuild/linux-s390x@0.28.1':
+    optional: true
+
+  '@esbuild/linux-x64@0.18.20':
+    optional: true
+
+  '@esbuild/linux-x64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-x64@0.28.1':
+    optional: true
+
+  '@esbuild/netbsd-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/netbsd-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.18.20':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.25.12':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.28.1':
+    optional: true
+
+  '@esbuild/openbsd-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/openbsd-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.18.20':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.25.12':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.28.1':
+    optional: true
+
+  '@esbuild/openharmony-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/openharmony-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/sunos-x64@0.18.20':
+    optional: true
+
+  '@esbuild/sunos-x64@0.25.12':
+    optional: true
+
+  '@esbuild/sunos-x64@0.28.1':
+    optional: true
+
+  '@esbuild/win32-arm64@0.18.20':
+    optional: true
+
+  '@esbuild/win32-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/win32-arm64@0.28.1':
+    optional: true
+
+  '@esbuild/win32-ia32@0.18.20':
+    optional: true
+
+  '@esbuild/win32-ia32@0.25.12':
+    optional: true
+
+  '@esbuild/win32-ia32@0.28.1':
+    optional: true
+
+  '@esbuild/win32-x64@0.18.20':
+    optional: true
+
+  '@esbuild/win32-x64@0.25.12':
+    optional: true
+
+  '@esbuild/win32-x64@0.28.1':
+    optional: true
+
   '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))':
     dependencies:
       eslint: 9.39.4(jiti@2.7.0)
@@ -2961,6 +3950,64 @@ snapshots:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.5
 
+  '@libsql/client@0.17.4':
+    dependencies:
+      '@libsql/core': 0.17.4
+      '@libsql/hrana-client': 0.10.0
+      js-base64: 3.7.8
+      libsql: 0.5.29
+      promise-limit: 2.7.0
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
+  '@libsql/core@0.17.4':
+    dependencies:
+      js-base64: 3.7.8
+
+  '@libsql/darwin-arm64@0.5.29':
+    optional: true
+
+  '@libsql/darwin-x64@0.5.29':
+    optional: true
+
+  '@libsql/hrana-client@0.10.0':
+    dependencies:
+      '@libsql/isomorphic-ws': 0.1.5
+      js-base64: 3.7.8
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
+  '@libsql/isomorphic-ws@0.1.5':
+    dependencies:
+      '@types/ws': 8.18.1
+      ws: 8.21.0
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
+  '@libsql/linux-arm-gnueabihf@0.5.29':
+    optional: true
+
+  '@libsql/linux-arm-musleabihf@0.5.29':
+    optional: true
+
+  '@libsql/linux-arm64-gnu@0.5.29':
+    optional: true
+
+  '@libsql/linux-arm64-musl@0.5.29':
+    optional: true
+
+  '@libsql/linux-x64-gnu@0.5.29':
+    optional: true
+
+  '@libsql/linux-x64-musl@0.5.29':
+    optional: true
+
+  '@libsql/win32-x64-msvc@0.5.29':
+    optional: true
+
   '@napi-rs/wasm-runtime@1.1.5(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
     dependencies:
       '@emnapi/core': 1.10.0
@@ -2968,6 +4015,8 @@ snapshots:
       '@tybys/wasm-util': 0.10.2
     optional: true
 
+  '@neon-rs/load@0.0.4': {}
+
   '@next/env@16.2.9': {}
 
   '@next/eslint-plugin-next@16.2.9':
@@ -3014,6 +4063,8 @@ snapshots:
 
   '@oxc-project/types@0.133.0': {}
 
+  '@panva/hkdf@1.2.1': {}
+
   '@reactflow/background@11.3.14(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
     dependencies:
       '@reactflow/core': 11.11.4(@types/react@19.2.17)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
@@ -3379,6 +4430,10 @@ snapshots:
   '@types/trusted-types@2.0.7':
     optional: true
 
+  '@types/ws@8.18.1':
+    dependencies:
+      '@types/node': 20.19.43
+
   '@typescript-eslint/eslint-plugin@8.61.0(@typescript-eslint/parser@8.61.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)':
     dependencies:
       '@eslint-community/regexpp': 4.12.2
@@ -3554,13 +4609,13 @@ snapshots:
       chai: 6.2.2
       tinyrainbow: 3.1.0
 
-  '@vitest/mocker@4.1.9(vite@8.0.16(@types/node@20.19.43)(jiti@2.7.0))':
+  '@vitest/mocker@4.1.9(vite@8.0.16(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4))':
     dependencies:
       '@vitest/spy': 4.1.9
       estree-walker: 3.0.3
       magic-string: 0.30.21
     optionalDependencies:
-      vite: 8.0.16(@types/node@20.19.43)(jiti@2.7.0)
+      vite: 8.0.16(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)
 
   '@vitest/pretty-format@4.1.9':
     dependencies:
@@ -3718,6 +4773,8 @@ snapshots:
       node-releases: 2.0.47
       update-browserslist-db: 1.2.3(browserslist@4.28.2)
 
+  buffer-from@1.1.2: {}
+
   call-bind-apply-helpers@1.0.2:
     dependencies:
       es-errors: 1.3.0
@@ -3866,6 +4923,8 @@ snapshots:
       has-property-descriptors: 1.0.2
       object-keys: 1.1.1
 
+  detect-libc@2.0.2: {}
+
   detect-libc@2.1.2: {}
 
   doctrine@2.1.0:
@@ -3877,6 +4936,17 @@ snapshots:
       '@types/trusted-types': 2.0.7
     optional: true
 
+  drizzle-kit@0.31.10:
+    dependencies:
+      '@drizzle-team/brocli': 0.10.2
+      '@esbuild-kit/esm-loader': 2.6.5
+      esbuild: 0.25.12
+      tsx: 4.22.4
+
+  drizzle-orm@0.45.2(@libsql/client@0.17.4):
+    optionalDependencies:
+      '@libsql/client': 0.17.4
+
   dunder-proto@1.0.1:
     dependencies:
       call-bind-apply-helpers: 1.0.2
@@ -3995,6 +5065,89 @@ snapshots:
       is-date-object: 1.1.0
       is-symbol: 1.1.1
 
+  esbuild@0.18.20:
+    optionalDependencies:
+      '@esbuild/android-arm': 0.18.20
+      '@esbuild/android-arm64': 0.18.20
+      '@esbuild/android-x64': 0.18.20
+      '@esbuild/darwin-arm64': 0.18.20
+      '@esbuild/darwin-x64': 0.18.20
+      '@esbuild/freebsd-arm64': 0.18.20
+      '@esbuild/freebsd-x64': 0.18.20
+      '@esbuild/linux-arm': 0.18.20
+      '@esbuild/linux-arm64': 0.18.20
+      '@esbuild/linux-ia32': 0.18.20
+      '@esbuild/linux-loong64': 0.18.20
+      '@esbuild/linux-mips64el': 0.18.20
+      '@esbuild/linux-ppc64': 0.18.20
+      '@esbuild/linux-riscv64': 0.18.20
+      '@esbuild/linux-s390x': 0.18.20
+      '@esbuild/linux-x64': 0.18.20
+      '@esbuild/netbsd-x64': 0.18.20
+      '@esbuild/openbsd-x64': 0.18.20
+      '@esbuild/sunos-x64': 0.18.20
+      '@esbuild/win32-arm64': 0.18.20
+      '@esbuild/win32-ia32': 0.18.20
+      '@esbuild/win32-x64': 0.18.20
+
+  esbuild@0.25.12:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.25.12
+      '@esbuild/android-arm': 0.25.12
+      '@esbuild/android-arm64': 0.25.12
+      '@esbuild/android-x64': 0.25.12
+      '@esbuild/darwin-arm64': 0.25.12
+      '@esbuild/darwin-x64': 0.25.12
+      '@esbuild/freebsd-arm64': 0.25.12
+      '@esbuild/freebsd-x64': 0.25.12
+      '@esbuild/linux-arm': 0.25.12
+      '@esbuild/linux-arm64': 0.25.12
+      '@esbuild/linux-ia32': 0.25.12
+      '@esbuild/linux-loong64': 0.25.12
+      '@esbuild/linux-mips64el': 0.25.12
+      '@esbuild/linux-ppc64': 0.25.12
+      '@esbuild/linux-riscv64': 0.25.12
+      '@esbuild/linux-s390x': 0.25.12
+      '@esbuild/linux-x64': 0.25.12
+      '@esbuild/netbsd-arm64': 0.25.12
+      '@esbuild/netbsd-x64': 0.25.12
+      '@esbuild/openbsd-arm64': 0.25.12
+      '@esbuild/openbsd-x64': 0.25.12
+      '@esbuild/openharmony-arm64': 0.25.12
+      '@esbuild/sunos-x64': 0.25.12
+      '@esbuild/win32-arm64': 0.25.12
+      '@esbuild/win32-ia32': 0.25.12
+      '@esbuild/win32-x64': 0.25.12
+
+  esbuild@0.28.1:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.28.1
+      '@esbuild/android-arm': 0.28.1
+      '@esbuild/android-arm64': 0.28.1
+      '@esbuild/android-x64': 0.28.1
+      '@esbuild/darwin-arm64': 0.28.1
+      '@esbuild/darwin-x64': 0.28.1
+      '@esbuild/freebsd-arm64': 0.28.1
+      '@esbuild/freebsd-x64': 0.28.1
+      '@esbuild/linux-arm': 0.28.1
+      '@esbuild/linux-arm64': 0.28.1
+      '@esbuild/linux-ia32': 0.28.1
+      '@esbuild/linux-loong64': 0.28.1
+      '@esbuild/linux-mips64el': 0.28.1
+      '@esbuild/linux-ppc64': 0.28.1
+      '@esbuild/linux-riscv64': 0.28.1
+      '@esbuild/linux-s390x': 0.28.1
+      '@esbuild/linux-x64': 0.28.1
+      '@esbuild/netbsd-arm64': 0.28.1
+      '@esbuild/netbsd-x64': 0.28.1
+      '@esbuild/openbsd-arm64': 0.28.1
+      '@esbuild/openbsd-x64': 0.28.1
+      '@esbuild/openharmony-arm64': 0.28.1
+      '@esbuild/sunos-x64': 0.28.1
+      '@esbuild/win32-arm64': 0.28.1
+      '@esbuild/win32-ia32': 0.28.1
+      '@esbuild/win32-x64': 0.28.1
+
   escalade@3.2.0: {}
 
   escape-string-regexp@4.0.0: {}
@@ -4524,6 +5677,10 @@ snapshots:
 
   jiti@2.7.0: {}
 
+  jose@6.2.3: {}
+
+  js-base64@3.7.8: {}
+
   js-tokens@4.0.0: {}
 
   js-yaml@4.2.0:
@@ -4577,6 +5734,21 @@ snapshots:
       prelude-ls: 1.2.1
       type-check: 0.4.0
 
+  libsql@0.5.29:
+    dependencies:
+      '@neon-rs/load': 0.0.4
+      detect-libc: 2.0.2
+    optionalDependencies:
+      '@libsql/darwin-arm64': 0.5.29
+      '@libsql/darwin-x64': 0.5.29
+      '@libsql/linux-arm-gnueabihf': 0.5.29
+      '@libsql/linux-arm-musleabihf': 0.5.29
+      '@libsql/linux-arm64-gnu': 0.5.29
+      '@libsql/linux-arm64-musl': 0.5.29
+      '@libsql/linux-x64-gnu': 0.5.29
+      '@libsql/linux-x64-musl': 0.5.29
+      '@libsql/win32-x64-msvc': 0.5.29
+
   lightningcss-android-arm64@1.32.0:
     optional: true
 
@@ -4671,6 +5843,12 @@ snapshots:
 
   natural-compare@1.4.0: {}
 
+  next-auth@5.0.0-beta.31(next@16.2.9(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4):
+    dependencies:
+      '@auth/core': 0.41.2
+      next: 16.2.9(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+      react: 19.2.4
+
   next@16.2.9(@babel/core@7.29.7)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
     dependencies:
       '@next/env': 16.2.9
@@ -4704,6 +5882,8 @@ snapshots:
 
   node-releases@2.0.47: {}
 
+  oauth4webapi@3.8.6: {}
+
   object-assign@4.1.1: {}
 
   object-inspect@1.13.4: {}
@@ -4808,10 +5988,18 @@ snapshots:
       picocolors: 1.1.1
       source-map-js: 1.2.1
 
+  preact-render-to-string@6.5.11(preact@10.24.3):
+    dependencies:
+      preact: 10.24.3
+
+  preact@10.24.3: {}
+
   prelude-ls@1.2.1: {}
 
   prettier@3.8.4: {}
 
+  promise-limit@2.7.0: {}
+
   prop-types@15.8.1:
     dependencies:
       loose-envify: 1.4.0
@@ -5033,6 +6221,13 @@ snapshots:
 
   source-map-js@1.2.1: {}
 
+  source-map-support@0.5.21:
+    dependencies:
+      buffer-from: 1.1.2
+      source-map: 0.6.1
+
+  source-map@0.6.1: {}
+
   stable-hash@0.0.5: {}
 
   stackback@0.0.2: {}
@@ -5157,6 +6352,12 @@ snapshots:
 
   tslib@2.8.1: {}
 
+  tsx@4.22.4:
+    dependencies:
+      esbuild: 0.28.1
+    optionalDependencies:
+      fsevents: 2.3.3
+
   type-check@0.4.0:
     dependencies:
       prelude-ls: 1.2.1
@@ -5262,7 +6463,7 @@ snapshots:
       base64-arraybuffer: 1.0.2
     optional: true
 
-  vite@8.0.16(@types/node@20.19.43)(jiti@2.7.0):
+  vite@8.0.16(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4):
     dependencies:
       lightningcss: 1.32.0
       picomatch: 4.0.4
@@ -5271,13 +6472,15 @@ snapshots:
       tinyglobby: 0.2.17
     optionalDependencies:
       '@types/node': 20.19.43
+      esbuild: 0.28.1
       fsevents: 2.3.3
       jiti: 2.7.0
+      tsx: 4.22.4
 
-  vitest@4.1.9(@types/node@20.19.43)(vite@8.0.16(@types/node@20.19.43)(jiti@2.7.0)):
+  vitest@4.1.9(@types/node@20.19.43)(vite@8.0.16(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)):
     dependencies:
       '@vitest/expect': 4.1.9
-      '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@20.19.43)(jiti@2.7.0))
+      '@vitest/mocker': 4.1.9(vite@8.0.16(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4))
       '@vitest/pretty-format': 4.1.9
       '@vitest/runner': 4.1.9
       '@vitest/snapshot': 4.1.9
@@ -5294,7 +6497,7 @@ snapshots:
       tinyexec: 1.2.4
       tinyglobby: 0.2.17
       tinyrainbow: 3.1.0
-      vite: 8.0.16(@types/node@20.19.43)(jiti@2.7.0)
+      vite: 8.0.16(@types/node@20.19.43)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)
       why-is-node-running: 2.3.0
     optionalDependencies:
       '@types/node': 20.19.43
@@ -5355,6 +6558,8 @@ snapshots:
 
   word-wrap@1.2.5: {}
 
+  ws@8.21.0: {}
+
   yallist@3.1.1: {}
 
   yocto-queue@0.1.0: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index c6e5cb0..efc037a 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,4 +1,2 @@
-allowBuilds:
-  core-js: false
-  sharp: false
-  unrs-resolver: false
+onlyBuiltDependencies:
+  - esbuild
diff --git a/src/app/[lang]/app/page.tsx b/src/app/[lang]/app/page.tsx
index ac4036b..c70bb5e 100644
--- a/src/app/[lang]/app/page.tsx
+++ b/src/app/[lang]/app/page.tsx
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
 import { notFound } from "next/navigation";
 import { getDictionary } from "@/i18n/dictionaries";
 import { isLocale } from "@/i18n/config";
+import { auth } from "@/auth";
 import CodeWorkspace from "@/components/ui/CodeWorkspace";
 
 export const metadata: Metadata = {
@@ -16,6 +17,7 @@ export default async function AppPage({
   const { lang } = await params;
   if (!isLocale(lang)) notFound();
   const t = getDictionary(lang);
+  const session = await auth();
 
   return (
     <>
@@ -31,6 +33,10 @@ export default async function AppPage({
         projectTab={t.projectTab}
         uploadFolder={t.uploadFolder}
         projectHint={t.projectHint}
+        isAuthed={!!session?.user}
+        saveLabel={t.auth.save}
+        savedLabel={t.auth.saved}
+        signInToSaveLabel={t.auth.signInToSave}
       />
     </>
   );
diff --git a/src/app/[lang]/graphs/[id]/page.tsx b/src/app/[lang]/graphs/[id]/page.tsx
new file mode 100644
index 0000000..d5635f4
--- /dev/null
+++ b/src/app/[lang]/graphs/[id]/page.tsx
@@ -0,0 +1,59 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { isLocale } from "@/i18n/config";
+import { getDictionary } from "@/i18n/dictionaries";
+import { auth } from "@/auth";
+import { getGraph } from "@/lib/graphs";
+import Diagram from "@/components/ui/Diagram";
+import type { Graph } from "@/lib/analysis/types";
+
+export const metadata: Metadata = {
+  title: "Weftmap — Saved graph",
+};
+
+export default async function SavedGraphPage({
+  params,
+}: {
+  params: Promise<{ lang: string; id: string }>;
+}) {
+  const { lang, id } = await params;
+  if (!isLocale(lang)) notFound();
+  const t = getDictionary(lang);
+
+  const session = await auth();
+  if (!session?.user) notFound();
+
+  const row = await getGraph(id);
+  if (!row) notFound();
+
+  const graph = JSON.parse(row.graph) as Graph;
+  const emptyLabel = row.language === "sql" ? t.noTables : t.noFunctions;
+
+  return (
+    <main className="flex h-[calc(100vh-65px)] flex-col">
+      <header className="flex items-center justify-between gap-4 border-b border-[#e2e8f0] dark:border-[#232a36] bg-[#f8fafc] dark:bg-[#12151c] px-4 py-3 sm:px-6">
+        <div className="min-w-0">
+          <h1 className="truncate font-semibold text-[#0f172a] dark:text-[#e6e9ef]">
+            {row.title}
+          </h1>
+          <span className="font-mono text-[12px] text-[#64748b] dark:text-[#7c8696]">
+            {row.language} · {graph.nodes.length} nodes · {graph.edges.length}{" "}
+            edges
+          </span>
+        </div>
+        <Link
+          href={`/${lang}/graphs`}
+          className="shrink-0 rounded-lg border border-[#e2e8f0] dark:border-[#232a36] px-3 py-1.5 text-[12px] text-[#475569] dark:text-[#9aa6b8] transition-colors hover:border-[#4f46e5] hover:text-[#0f172a]"
+        >
+          {t.auth.graphsTitle}
+        </Link>
+      </header>
+      <div className="relative flex-1 bg-[#f6f7f9] dark:bg-[#0b0d12]">
+        <div className="absolute inset-0">
+          <Diagram graph={graph} emptyLabel={emptyLabel} />
+        </div>
+      </div>
+    </main>
+  );
+}
diff --git a/src/app/[lang]/graphs/page.tsx b/src/app/[lang]/graphs/page.tsx
new file mode 100644
index 0000000..74f4917
--- /dev/null
+++ b/src/app/[lang]/graphs/page.tsx
@@ -0,0 +1,71 @@
+import type { Metadata } from "next";
+import { notFound } from "next/navigation";
+import { isLocale, type Locale } from "@/i18n/config";
+import { getDictionary } from "@/i18n/dictionaries";
+import { auth } from "@/auth";
+import { listGraphs } from "@/lib/graphs";
+import GraphListItem from "@/components/ui/GraphListItem";
+
+export const metadata: Metadata = {
+  title: "Weftmap — My graphs",
+};
+
+export default async function GraphsPage({
+  params,
+}: {
+  params: Promise<{ lang: string }>;
+}) {
+  const { lang } = await params;
+  if (!isLocale(lang)) notFound();
+  const t = getDictionary(lang);
+  const session = await auth();
+
+  return (
+    <main className="mx-auto w-full max-w-3xl px-4 py-12 sm:px-6">
+      <h1 className="mb-6 text-2xl font-bold text-[#0f172a] dark:text-[#e6e9ef]">
+        {t.auth.graphsTitle}
+      </h1>
+
+      {!session?.user ? (
+        <p className="text-[#475569] dark:text-[#9aa6b8]">
+          {t.auth.graphsSignIn}
+        </p>
+      ) : (
+        <GraphList lang={lang} t={t} />
+      )}
+    </main>
+  );
+}
+
+async function GraphList({
+  lang,
+  t,
+}: {
+  lang: Locale;
+  t: ReturnType<typeof getDictionary>;
+}) {
+  const graphs = await listGraphs();
+
+  if (graphs.length === 0) {
+    return (
+      <p className="text-[#475569] dark:text-[#9aa6b8]">{t.auth.graphsEmpty}</p>
+    );
+  }
+
+  return (
+    <ul className="divide-y divide-[#eef1f5] dark:divide-[#232a36] overflow-hidden rounded-xl border border-[#e2e8f0] dark:border-[#232a36]">
+      {graphs.map((g) => (
+        <GraphListItem
+          key={g.id}
+          id={g.id}
+          title={g.title}
+          language={g.language}
+          dateLabel={new Date(g.createdAt).toLocaleDateString(lang)}
+          href={`/${lang}/graphs/${g.id}`}
+          openLabel={t.auth.open}
+          deleteLabel={t.auth.delete}
+        />
+      ))}
+    </ul>
+  );
+}
diff --git a/src/app/[lang]/layout.tsx b/src/app/[lang]/layout.tsx
index 1632bae..5247bda 100644
--- a/src/app/[lang]/layout.tsx
+++ b/src/app/[lang]/layout.tsx
@@ -5,6 +5,7 @@ import { notFound } from "next/navigation";
 import { locales, isLocale, type Locale } from "@/i18n/config";
 import { getDictionary } from "@/i18n/dictionaries";
 import { Analytics } from "@vercel/analytics/next";
+import { auth } from "@/auth";
 import Header from "@/components/layout/Header";
 import Footer from "@/components/layout/Footer";
 import "@fontsource-variable/lexend";
@@ -30,6 +31,7 @@ export default async function RootLayout({
   if (!isLocale(lang)) notFound();
   const t = getDictionary(lang as Locale);
   const nonce = (await headers()).get("x-nonce") ?? undefined;
+  const session = await auth();
 
   return (
     <html
@@ -51,7 +53,7 @@ export default async function RootLayout({
       </head>
       <body>
         <div className="grid-bg" aria-hidden="true" />
-        <Header lang={lang} />
+        <Header lang={lang} user={session?.user ?? null} />
         {children}
         <Footer
           lang={lang}
diff --git a/src/app/[lang]/page.tsx b/src/app/[lang]/page.tsx
index 73a742a..74e4cdc 100644
--- a/src/app/[lang]/page.tsx
+++ b/src/app/[lang]/page.tsx
@@ -8,6 +8,7 @@ import Showcase from "@/components/sections/Showcase";
 import SupportedLanguages from "@/components/sections/SupportedLanguages";
 import UseCases from "@/components/sections/UseCases";
 import Faq from "@/components/sections/Faq";
+import Testimonials from "@/components/sections/Testimonials";
 import CallToAction from "@/components/sections/CallToAction";
 import ScrollReveal from "@/components/ui/ScrollReveal";
 
@@ -52,6 +53,7 @@ export default async function Home({
       />
       <UseCases title={t.useCasesTitle} items={t.useCases} />
       <Faq title={t.faqTitle} items={t.faqs} />
+      <Testimonials lang={lang} />
       <CallToAction
         title={t.ctaTitle}
         desc={t.ctaDesc}
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000..046cba4
--- /dev/null
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,6 @@
+import { handlers } from "@/auth";
+
+// libSQL/Drizzle need the Node runtime.
+export const runtime = "nodejs";
+
+export const { GET, POST } = handlers;
diff --git a/src/auth.ts b/src/auth.ts
new file mode 100644
index 0000000..7eff933
--- /dev/null
+++ b/src/auth.ts
@@ -0,0 +1,28 @@
+import NextAuth, { type DefaultSession } from "next-auth";
+import GitHub from "next-auth/providers/github";
+import { DrizzleAdapter } from "@auth/drizzle-adapter";
+import { db } from "@/db";
+import { accounts, sessions, users, verificationTokens } from "@/db/schema";
+
+declare module "next-auth" {
+  interface Session {
+    user: { id: string } & DefaultSession["user"];
+  }
+}
+
+export const { handlers, auth, signIn, signOut } = NextAuth({
+  adapter: DrizzleAdapter(db, {
+    usersTable: users,
+    accountsTable: accounts,
+    sessionsTable: sessions,
+    verificationTokensTable: verificationTokens,
+  }),
+  providers: [GitHub],
+  callbacks: {
+    // Database session strategy exposes the adapter user; surface its id.
+    session({ session, user }) {
+      session.user.id = user.id;
+      return session;
+    },
+  },
+});
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
index bc14153..1785ed0 100644
--- a/src/components/layout/Header.tsx
+++ b/src/components/layout/Header.tsx
@@ -5,9 +5,12 @@ import Link from "next/link";
 import { usePathname } from "next/navigation";
 import { locales, type Locale } from "@/i18n/config";
 import { getDictionary } from "@/i18n/dictionaries";
+import { signInGitHub, signOutAction } from "@/lib/auth-actions";
 import Logo from "./Logo";
 import ThemeToggle from "./ThemeToggle";
 
+type HeaderUser = { name?: string | null; image?: string | null };
+
 const REPO = "https://github.com/DataDave-Dev/weftmap";
 
 const SCROLL_THRESHOLD = 8;
@@ -31,7 +34,13 @@ const LANGUAGE_NAMES: Record<string, string> = {
   it: "Italiano",
 };
 
-export default function Header({ lang }: { lang: Locale }) {
+export default function Header({
+  lang,
+  user,
+}: {
+  lang: Locale;
+  user: HeaderUser | null;
+}) {
   const t = getDictionary(lang);
   const [dropdownOpen, setDropdownOpen] = useState(false);
   const pathname = usePathname();
@@ -123,6 +132,42 @@ export default function Header({ lang }: { lang: Locale }) {
           {t.getStarted}
         </Link>
 
+        {user ? (
+          <div className="flex shrink-0 items-center gap-1">
+            <Link href={`/${lang}/graphs`} className={linkClass}>
+              {t.auth.myGraphs}
+            </Link>
+            <form action={signOutAction}>
+              <button
+                type="submit"
+                title={t.auth.signOut}
+                className="flex items-center gap-2 rounded-full p-0.5 pr-2 text-[13px] text-[#475569] transition-colors hover:bg-black/[0.04] hover:text-[#0f172a] dark:text-[#94a3b8] dark:hover:bg-white/[0.06] dark:hover:text-[#e6e9ef]"
+              >
+                {user.image ? (
+                  // eslint-disable-next-line @next/next/no-img-element
+                  <img
+                    src={user.image}
+                    alt={user.name ?? ""}
+                    width={26}
+                    height={26}
+                    className="rounded-full"
+                  />
+                ) : null}
+                <span className="hidden sm:inline">{t.auth.signOut}</span>
+              </button>
+            </form>
+          </div>
+        ) : (
+          <form action={signInGitHub} className="shrink-0">
+            <button
+              type="submit"
+              className="whitespace-nowrap rounded-full border border-[#e2e8f0] px-3.5 py-1.5 text-[13px] font-semibold text-[#0f172a] transition hover:bg-black/[0.04] dark:border-white/[0.14] dark:text-[#e6e9ef] dark:hover:bg-white/[0.06]"
+            >
+              {t.auth.signIn}
+            </button>
+          </form>
+        )}
+
         <div className="relative shrink-0">
           <button
             onClick={() => setDropdownOpen(!dropdownOpen)}
diff --git a/src/components/sections/TestimonialForm.tsx b/src/components/sections/TestimonialForm.tsx
new file mode 100644
index 0000000..385bcd3
--- /dev/null
+++ b/src/components/sections/TestimonialForm.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import { useActionState, useEffect, useRef } from "react";
+import { useRouter } from "next/navigation";
+import {
+  postTestimonial,
+  type TestimonialFormState,
+} from "@/lib/testimonials";
+
+const INITIAL: TestimonialFormState = { ok: false };
+
+type Props = {
+  placeholder: string;
+  submit: string;
+  thanks: string;
+};
+
+export default function TestimonialForm({ placeholder, submit, thanks }: Props) {
+  const router = useRouter();
+  const [state, action, pending] = useActionState(postTestimonial, INITIAL);
+  const formRef = useRef<HTMLFormElement>(null);
+
+  useEffect(() => {
+    if (state.ok) {
+      formRef.current?.reset();
+      router.refresh();
+    }
+  }, [state, router]);
+
+  return (
+    <form ref={formRef} action={action} className="flex flex-col gap-3">
+      <textarea
+        name="body"
+        required
+        minLength={3}
+        maxLength={1000}
+        rows={3}
+        placeholder={placeholder}
+        className="w-full resize-none rounded-xl border border-[#e2e8f0] dark:border-[#232a36] bg-white dark:bg-[#12151c] px-4 py-3 text-[15px] text-[#0f172a] dark:text-[#e6e9ef] outline-none focus-visible:border-[#4f46e5]"
+      />
+      <div className="flex items-center gap-3">
+        <button
+          type="submit"
+          disabled={pending}
+          className="rounded-full bg-[#4f46e5] dark:bg-[#6366f1] px-5 py-2 text-sm font-semibold text-white transition hover:-translate-y-px disabled:opacity-50"
+        >
+          {submit}
+        </button>
+        {state.ok && (
+          <span className="text-[13px] text-emerald-600 dark:text-emerald-400">
+            {thanks}
+          </span>
+        )}
+      </div>
+    </form>
+  );
+}
diff --git a/src/components/sections/Testimonials.tsx b/src/components/sections/Testimonials.tsx
new file mode 100644
index 0000000..eaaa563
--- /dev/null
+++ b/src/components/sections/Testimonials.tsx
@@ -0,0 +1,70 @@
+import { auth } from "@/auth";
+import { listTestimonials } from "@/lib/testimonials";
+import { getDictionary } from "@/i18n/dictionaries";
+import type { Locale } from "@/i18n/config";
+import SectionHeading from "./SectionHeading";
+import TestimonialForm from "./TestimonialForm";
+
+export default async function Testimonials({ lang }: { lang: Locale }) {
+  const t = getDictionary(lang);
+  const [session, items] = await Promise.all([auth(), listTestimonials()]);
+
+  return (
+    <section id="testimonials" className="bg-white dark:bg-[#0b0d12]">
+      <div className="mx-auto w-full max-w-[1100px] px-6 py-24 max-[620px]:py-16">
+        <SectionHeading index="08" title={t.auth.testimonialsTitle} />
+        <p className="mt-4 max-w-[48ch] text-[15px] leading-[1.65] text-[#475569] dark:text-[#9aa6b8]">
+          {t.auth.testimonialsSubtitle}
+        </p>
+
+        <div className="mt-10 max-w-2xl">
+          {session?.user ? (
+            <TestimonialForm
+              placeholder={t.auth.testimonialPlaceholder}
+              submit={t.auth.testimonialSubmit}
+              thanks={t.auth.testimonialThanks}
+            />
+          ) : (
+            <p className="text-[15px] text-[#475569] dark:text-[#9aa6b8]">
+              {t.auth.testimonialSignIn}
+            </p>
+          )}
+        </div>
+
+        {items.length === 0 ? (
+          <p className="mt-10 text-[15px] text-[#94a3b8]">
+            {t.auth.testimonialsEmpty}
+          </p>
+        ) : (
+          <ul className="mt-12 grid grid-cols-2 gap-6 max-[620px]:grid-cols-1">
+            {items.map((item) => (
+              <li
+                key={item.id}
+                className="flex flex-col gap-4 rounded-2xl border border-[#e2e8f0] dark:border-[#232a36] bg-white dark:bg-[#12151c] p-6"
+              >
+                <p className="text-[15px] leading-[1.65] text-[#0f172a] dark:text-[#e6e9ef]">
+                  {item.body}
+                </p>
+                <div className="flex items-center gap-3">
+                  {item.userImage ? (
+                    // eslint-disable-next-line @next/next/no-img-element
+                    <img
+                      src={item.userImage}
+                      alt=""
+                      width={32}
+                      height={32}
+                      className="rounded-full"
+                    />
+                  ) : null}
+                  <span className="text-[13px] font-medium text-[#475569] dark:text-[#9aa6b8]">
+                    {item.userName ?? "—"}
+                  </span>
+                </div>
+              </li>
+            ))}
+          </ul>
+        )}
+      </div>
+    </section>
+  );
+}
diff --git a/src/components/ui/CodeWorkspace.tsx b/src/components/ui/CodeWorkspace.tsx
index 95f78e8..0ab5ead 100644
--- a/src/components/ui/CodeWorkspace.tsx
+++ b/src/components/ui/CodeWorkspace.tsx
@@ -2,6 +2,7 @@
 
 import { useEffect, useMemo, useRef, useState } from "react";
 import Diagram from "./Diagram";
+import { saveGraph } from "@/lib/graphs";
 import type { Graph } from "@/lib/analysis/types";
 
 const LANGUAGES = ["python", "javascript", "typescript", "go", "rust", "sql"];
@@ -180,6 +181,10 @@ type Props = {
   projectTab: string;
   uploadFolder: string;
   projectHint: string;
+  isAuthed: boolean;
+  saveLabel: string;
+  savedLabel: string;
+  signInToSaveLabel: string;
 };
 
 export default function CodeWorkspace({
@@ -194,6 +199,10 @@ export default function CodeWorkspace({
   projectTab,
   uploadFolder,
   projectHint,
+  isAuthed,
+  saveLabel,
+  savedLabel,
+  signInToSaveLabel,
 }: Props) {
   const [mode, setMode] = useState<"snippet" | "project">("snippet");
   const [code, setCode] = useState(SAMPLES.python);
@@ -202,6 +211,8 @@ export default function CodeWorkspace({
   const [graph, setGraph] = useState<Graph | null>(null);
   const [isLoading, setIsLoading] = useState(false);
   const [error, setError] = useState<string | null>(null);
+  const [saving, setSaving] = useState(false);
+  const [saved, setSaved] = useState(false);
   const [editorWidth, setEditorWidth] = useState(44); // % of the split, lg+ only
   const gutterRef = useRef<HTMLDivElement>(null);
   const containerRef = useRef<HTMLDivElement>(null);
@@ -295,6 +306,30 @@ export default function CodeWorkspace({
     }
   }
 
+  async function handleSave() {
+    if (!graph || saving) return;
+    const fallback = `${language} · ${new Date().toLocaleDateString()}`;
+    const title = window.prompt(saveLabel, fallback);
+    if (title === null) return; // cancelled
+    const source =
+      mode === "project" ? { files: files ?? [] } : { code: code.trim() };
+    setSaving(true);
+    try {
+      await saveGraph({
+        title: title.trim() || fallback,
+        language,
+        graph,
+        source,
+      });
+      setSaved(true);
+      window.setTimeout(() => setSaved(false), 2500);
+    } catch {
+      window.alert("Error");
+    } finally {
+      setSaving(false);
+    }
+  }
+
   function onLanguageChange(e: React.ChangeEvent<HTMLSelectElement>) {
     const next = e.target.value;
     setLanguage(next);
@@ -500,9 +535,24 @@ export default function CodeWorkspace({
             </span>
           </div>
           {graph && (
-            <span className="font-mono text-[11px] text-[#64748b] dark:text-[#7c8696]">
-              {graph.nodes.length} nodes · {graph.edges.length} edges
-            </span>
+            <div className="flex items-center gap-3">
+              <span className="font-mono text-[11px] text-[#64748b] dark:text-[#7c8696]">
+                {graph.nodes.length} nodes · {graph.edges.length} edges
+              </span>
+              {isAuthed ? (
+                <button
+                  onClick={handleSave}
+                  disabled={saving}
+                  className="rounded-full border border-[#e2e8f0] dark:border-[#232a36] px-3 py-1 text-[12px] font-medium text-[#475569] dark:text-[#9aa6b8] transition-colors hover:border-[#4f46e5] hover:text-[#0f172a] disabled:opacity-50"
+                >
+                  {saved ? savedLabel : saveLabel}
+                </button>
+              ) : (
+                <span className="text-[11px] text-[#94a3b8]">
+                  {signInToSaveLabel}
+                </span>
+              )}
+            </div>
           )}
         </div>
 
diff --git a/src/components/ui/GraphListItem.tsx b/src/components/ui/GraphListItem.tsx
new file mode 100644
index 0000000..e0d0969
--- /dev/null
+++ b/src/components/ui/GraphListItem.tsx
@@ -0,0 +1,68 @@
+"use client";
+
+import { useState, useTransition } from "react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { deleteGraph } from "@/lib/graphs";
+
+type Props = {
+  id: string;
+  title: string;
+  language: string;
+  dateLabel: string;
+  href: string;
+  openLabel: string;
+  deleteLabel: string;
+};
+
+export default function GraphListItem({
+  id,
+  title,
+  language,
+  dateLabel,
+  href,
+  openLabel,
+  deleteLabel,
+}: Props) {
+  const router = useRouter();
+  const [isPending, startTransition] = useTransition();
+  const [removed, setRemoved] = useState(false);
+
+  function onDelete() {
+    startTransition(async () => {
+      await deleteGraph(id);
+      setRemoved(true);
+      router.refresh();
+    });
+  }
+
+  if (removed) return null;
+
+  return (
+    <li className="flex items-center justify-between gap-4 px-4 py-3">
+      <Link href={href} className="min-w-0 flex-1">
+        <span className="block truncate font-medium text-[#0f172a] dark:text-[#e6e9ef]">
+          {title}
+        </span>
+        <span className="font-mono text-[12px] text-[#64748b] dark:text-[#7c8696]">
+          {language} · {dateLabel}
+        </span>
+      </Link>
+      <div className="flex shrink-0 items-center gap-2">
+        <Link
+          href={href}
+          className="rounded-lg border border-[#e2e8f0] dark:border-[#232a36] px-3 py-1.5 text-[12px] text-[#475569] dark:text-[#9aa6b8] transition-colors hover:border-[#4f46e5] hover:text-[#0f172a]"
+        >
+          {openLabel}
+        </Link>
+        <button
+          onClick={onDelete}
+          disabled={isPending}
+          className="rounded-lg border border-transparent px-3 py-1.5 text-[12px] text-red-500 transition-colors hover:bg-red-500/10 disabled:opacity-50"
+        >
+          {deleteLabel}
+        </button>
+      </div>
+    </li>
+  );
+}
diff --git a/src/db/index.ts b/src/db/index.ts
new file mode 100644
index 0000000..429cd8e
--- /dev/null
+++ b/src/db/index.ts
@@ -0,0 +1,16 @@
+import "server-only";
+import { createClient } from "@libsql/client";
+import { drizzle } from "drizzle-orm/libsql";
+import * as schema from "./schema";
+
+const url = process.env.TURSO_DATABASE_URL;
+if (!url) {
+  throw new Error("TURSO_DATABASE_URL is not set");
+}
+
+const client = createClient({
+  url,
+  authToken: process.env.TURSO_AUTH_TOKEN,
+});
+
+export const db = drizzle(client, { schema });
diff --git a/src/db/schema.ts b/src/db/schema.ts
new file mode 100644
index 0000000..750524d
--- /dev/null
+++ b/src/db/schema.ts
@@ -0,0 +1,95 @@
+import {
+  integer,
+  primaryKey,
+  sqliteTable,
+  text,
+} from "drizzle-orm/sqlite-core";
+import type { AdapterAccountType } from "next-auth/adapters";
+
+// ── Auth.js core tables (shape required by @auth/drizzle-adapter) ──
+
+export const users = sqliteTable("user", {
+  id: text("id")
+    .primaryKey()
+    .$defaultFn(() => crypto.randomUUID()),
+  name: text("name"),
+  email: text("email").unique(),
+  emailVerified: integer("emailVerified", { mode: "timestamp_ms" }),
+  image: text("image"),
+});
+
+export const accounts = sqliteTable(
+  "account",
+  {
+    userId: text("userId")
+      .notNull()
+      .references(() => users.id, { onDelete: "cascade" }),
+    type: text("type").$type<AdapterAccountType>().notNull(),
+    provider: text("provider").notNull(),
+    providerAccountId: text("providerAccountId").notNull(),
+    refresh_token: text("refresh_token"),
+    access_token: text("access_token"),
+    expires_at: integer("expires_at"),
+    token_type: text("token_type"),
+    scope: text("scope"),
+    id_token: text("id_token"),
+    session_state: text("session_state"),
+  },
+  (account) => [
+    primaryKey({
+      columns: [account.provider, account.providerAccountId],
+    }),
+  ],
+);
+
+export const sessions = sqliteTable("session", {
+  sessionToken: text("sessionToken").primaryKey(),
+  userId: text("userId")
+    .notNull()
+    .references(() => users.id, { onDelete: "cascade" }),
+  expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
+});
+
+export const verificationTokens = sqliteTable(
+  "verificationToken",
+  {
+    identifier: text("identifier").notNull(),
+    token: text("token").notNull(),
+    expires: integer("expires", { mode: "timestamp_ms" }).notNull(),
+  },
+  (vt) => [primaryKey({ columns: [vt.identifier, vt.token] })],
+);
+
+// ── Application tables ──
+
+// A saved analysis: the resulting graph snapshot plus the source that produced
+// it (so it can be reopened or re-analyzed). Private to its owner.
+export const graphs = sqliteTable("graph", {
+  id: text("id")
+    .primaryKey()
+    .$defaultFn(() => crypto.randomUUID()),
+  userId: text("userId")
+    .notNull()
+    .references(() => users.id, { onDelete: "cascade" }),
+  title: text("title").notNull(),
+  language: text("language").notNull(),
+  source: text("source").notNull(), // JSON: { code } | { files }
+  graph: text("graph").notNull(), // JSON: Graph (nodes + edges)
+  createdAt: integer("createdAt", { mode: "timestamp_ms" })
+    .notNull()
+    .$defaultFn(() => new Date()),
+});
+
+// Public testimonial wall: a logged-in user's experience using the site.
+export const testimonials = sqliteTable("testimonial", {
+  id: text("id")
+    .primaryKey()
+    .$defaultFn(() => crypto.randomUUID()),
+  userId: text("userId")
+    .notNull()
+    .references(() => users.id, { onDelete: "cascade" }),
+  body: text("body").notNull(),
+  createdAt: integer("createdAt", { mode: "timestamp_ms" })
+    .notNull()
+    .$defaultFn(() => new Date()),
+});
diff --git a/src/i18n/dictionaries/ar.json b/src/i18n/dictionaries/ar.json
index e230bec..dbc5247 100644
--- a/src/i18n/dictionaries/ar.json
+++ b/src/i18n/dictionaries/ar.json
@@ -156,5 +156,25 @@
   "notFoundTitle": "هذه الصفحة تاهت في المخطط",
   "notFoundDesc": "ربما الرابط تالف أو تم نقل الصفحة. لنعدك إلى المسار الصحيح.",
   "notFoundHome": "العودة للرئيسية",
-  "notFoundApp": "افتح التطبيق"
+  "notFoundApp": "افتح التطبيق",
+  "auth": {
+    "signIn": "تسجيل الدخول",
+    "signOut": "تسجيل الخروج",
+    "myGraphs": "مخططاتي",
+    "save": "حفظ المخطط",
+    "saved": "تم الحفظ ✓",
+    "signInToSave": "سجّل الدخول للحفظ",
+    "graphsTitle": "مخططاتي",
+    "graphsEmpty": "لم تحفظ أي مخطط بعد.",
+    "graphsSignIn": "سجّل الدخول لعرض مخططاتك المحفوظة.",
+    "open": "فتح",
+    "delete": "حذف",
+    "testimonialsTitle": "ماذا يقول الناس",
+    "testimonialsSubtitle": "تجارب من يستخدمون Weftmap.",
+    "testimonialPlaceholder": "شارك تجربتك في استخدام Weftmap…",
+    "testimonialSubmit": "نشر",
+    "testimonialSignIn": "سجّل الدخول لمشاركة تجربتك.",
+    "testimonialsEmpty": "كن أول من يشارك تجربته.",
+    "testimonialThanks": "شكرًا على المشاركة!"
+  }
 }
diff --git a/src/i18n/dictionaries/en.json b/src/i18n/dictionaries/en.json
index 1acf721..70138dc 100644
--- a/src/i18n/dictionaries/en.json
+++ b/src/i18n/dictionaries/en.json
@@ -152,5 +152,25 @@
   "notFoundTitle": "This page got lost in the graph",
   "notFoundDesc": "The link may be broken or the page may have moved. Let's get you back on track.",
   "notFoundHome": "Back home",
-  "notFoundApp": "Open the app"
+  "notFoundApp": "Open the app",
+  "auth": {
+    "signIn": "Sign in",
+    "signOut": "Sign out",
+    "myGraphs": "My graphs",
+    "save": "Save graph",
+    "saved": "Saved ✓",
+    "signInToSave": "Sign in to save",
+    "graphsTitle": "My graphs",
+    "graphsEmpty": "You haven't saved any graphs yet.",
+    "graphsSignIn": "Sign in to view your saved graphs.",
+    "open": "Open",
+    "delete": "Delete",
+    "testimonialsTitle": "What people say",
+    "testimonialsSubtitle": "Experiences from people using Weftmap.",
+    "testimonialPlaceholder": "Share your experience using Weftmap…",
+    "testimonialSubmit": "Post",
+    "testimonialSignIn": "Sign in to share your experience.",
+    "testimonialsEmpty": "Be the first to share your experience.",
+    "testimonialThanks": "Thanks for sharing!"
+  }
 }
diff --git a/src/i18n/dictionaries/es.json b/src/i18n/dictionaries/es.json
index bbaf60f..9d71a0d 100644
--- a/src/i18n/dictionaries/es.json
+++ b/src/i18n/dictionaries/es.json
@@ -152,5 +152,25 @@
   "notFoundTitle": "Esta página se perdió en el grafo",
   "notFoundDesc": "El enlace puede estar roto o la página se movió. Volvamos al camino.",
   "notFoundHome": "Volver al inicio",
-  "notFoundApp": "Abrir la app"
+  "notFoundApp": "Abrir la app",
+  "auth": {
+    "signIn": "Iniciar sesión",
+    "signOut": "Cerrar sesión",
+    "myGraphs": "Mis grafos",
+    "save": "Guardar grafo",
+    "saved": "Guardado ✓",
+    "signInToSave": "Inicia sesión para guardar",
+    "graphsTitle": "Mis grafos",
+    "graphsEmpty": "Todavía no has guardado ningún grafo.",
+    "graphsSignIn": "Inicia sesión para ver tus grafos guardados.",
+    "open": "Abrir",
+    "delete": "Eliminar",
+    "testimonialsTitle": "Lo que dice la gente",
+    "testimonialsSubtitle": "Experiencias de quienes usan Weftmap.",
+    "testimonialPlaceholder": "Comparte tu experiencia usando Weftmap…",
+    "testimonialSubmit": "Publicar",
+    "testimonialSignIn": "Inicia sesión para compartir tu experiencia.",
+    "testimonialsEmpty": "Sé el primero en compartir tu experiencia.",
+    "testimonialThanks": "¡Gracias por compartir!"
+  }
 }
diff --git a/src/i18n/dictionaries/fr.json b/src/i18n/dictionaries/fr.json
index 6489999..c216148 100644
--- a/src/i18n/dictionaries/fr.json
+++ b/src/i18n/dictionaries/fr.json
@@ -156,5 +156,25 @@
   "notFoundTitle": "Cette page s'est perdue dans le graphe",
   "notFoundDesc": "Le lien est peut-être cassé ou la page a été déplacée. Revenons sur le bon chemin.",
   "notFoundHome": "Retour à l'accueil",
-  "notFoundApp": "Ouvrir l'application"
+  "notFoundApp": "Ouvrir l'application",
+  "auth": {
+    "signIn": "Se connecter",
+    "signOut": "Se déconnecter",
+    "myGraphs": "Mes graphes",
+    "save": "Enregistrer le graphe",
+    "saved": "Enregistré ✓",
+    "signInToSave": "Connectez-vous pour enregistrer",
+    "graphsTitle": "Mes graphes",
+    "graphsEmpty": "Vous n'avez encore enregistré aucun graphe.",
+    "graphsSignIn": "Connectez-vous pour voir vos graphes enregistrés.",
+    "open": "Ouvrir",
+    "delete": "Supprimer",
+    "testimonialsTitle": "Ce que disent les gens",
+    "testimonialsSubtitle": "Témoignages de personnes utilisant Weftmap.",
+    "testimonialPlaceholder": "Partagez votre expérience avec Weftmap…",
+    "testimonialSubmit": "Publier",
+    "testimonialSignIn": "Connectez-vous pour partager votre expérience.",
+    "testimonialsEmpty": "Soyez le premier à partager votre expérience.",
+    "testimonialThanks": "Merci d'avoir partagé !"
+  }
 }
diff --git a/src/i18n/dictionaries/it.json b/src/i18n/dictionaries/it.json
index 55aa5d2..ee3e218 100644
--- a/src/i18n/dictionaries/it.json
+++ b/src/i18n/dictionaries/it.json
@@ -156,5 +156,25 @@
   "notFoundTitle": "Questa pagina si è persa nel grafo",
   "notFoundDesc": "Il link potrebbe essere interrotto o la pagina potrebbe essere stata spostata. Torniamo in pista.",
   "notFoundHome": "Torna alla home",
-  "notFoundApp": "Apri l'app"
+  "notFoundApp": "Apri l'app",
+  "auth": {
+    "signIn": "Accedi",
+    "signOut": "Esci",
+    "myGraphs": "I miei grafi",
+    "save": "Salva grafo",
+    "saved": "Salvato ✓",
+    "signInToSave": "Accedi per salvare",
+    "graphsTitle": "I miei grafi",
+    "graphsEmpty": "Non hai ancora salvato alcun grafo.",
+    "graphsSignIn": "Accedi per vedere i tuoi grafi salvati.",
+    "open": "Apri",
+    "delete": "Elimina",
+    "testimonialsTitle": "Cosa dicono le persone",
+    "testimonialsSubtitle": "Esperienze di chi usa Weftmap.",
+    "testimonialPlaceholder": "Condividi la tua esperienza con Weftmap…",
+    "testimonialSubmit": "Pubblica",
+    "testimonialSignIn": "Accedi per condividere la tua esperienza.",
+    "testimonialsEmpty": "Sii il primo a condividere la tua esperienza.",
+    "testimonialThanks": "Grazie per aver condiviso!"
+  }
 }
diff --git a/src/i18n/dictionaries/pt.json b/src/i18n/dictionaries/pt.json
index 419601f..7d65632 100644
--- a/src/i18n/dictionaries/pt.json
+++ b/src/i18n/dictionaries/pt.json
@@ -156,5 +156,25 @@
   "notFoundTitle": "Esta página se perdeu no grafo",
   "notFoundDesc": "O link pode estar quebrado ou a página pode ter sido movida. Vamos colocar você de volta nos trilhos.",
   "notFoundHome": "Voltar ao início",
-  "notFoundApp": "Abrir o aplicativo"
+  "notFoundApp": "Abrir o aplicativo",
+  "auth": {
+    "signIn": "Entrar",
+    "signOut": "Sair",
+    "myGraphs": "Meus grafos",
+    "save": "Salvar grafo",
+    "saved": "Salvo ✓",
+    "signInToSave": "Entre para salvar",
+    "graphsTitle": "Meus grafos",
+    "graphsEmpty": "Você ainda não salvou nenhum grafo.",
+    "graphsSignIn": "Entre para ver seus grafos salvos.",
+    "open": "Abrir",
+    "delete": "Excluir",
+    "testimonialsTitle": "O que as pessoas dizem",
+    "testimonialsSubtitle": "Experiências de quem usa o Weftmap.",
+    "testimonialPlaceholder": "Compartilhe sua experiência usando o Weftmap…",
+    "testimonialSubmit": "Publicar",
+    "testimonialSignIn": "Entre para compartilhar sua experiência.",
+    "testimonialsEmpty": "Seja o primeiro a compartilhar sua experiência.",
+    "testimonialThanks": "Obrigado por compartilhar!"
+  }
 }
diff --git a/src/lib/auth-actions.ts b/src/lib/auth-actions.ts
new file mode 100644
index 0000000..998cce4
--- /dev/null
+++ b/src/lib/auth-actions.ts
@@ -0,0 +1,11 @@
+"use server";
+
+import { signIn, signOut } from "@/auth";
+
+export async function signInGitHub() {
+  await signIn("github");
+}
+
+export async function signOutAction() {
+  await signOut();
+}
diff --git a/src/lib/graphs.ts b/src/lib/graphs.ts
new file mode 100644
index 0000000..6ef39ce
--- /dev/null
+++ b/src/lib/graphs.ts
@@ -0,0 +1,74 @@
+"use server";
+
+import { and, desc, eq } from "drizzle-orm";
+import { auth } from "@/auth";
+import { db } from "@/db";
+import { graphs } from "@/db/schema";
+import { graphMetaSchema, graphSchema, sourceSchema } from "./validation";
+
+async function requireUserId(): Promise<string> {
+  const session = await auth();
+  if (!session?.user?.id) throw new Error("UNAUTHORIZED");
+  return session.user.id;
+}
+
+export async function saveGraph(input: {
+  title: string;
+  language: string;
+  graph: unknown;
+  source: unknown;
+}): Promise<{ id: string }> {
+  const userId = await requireUserId();
+  const meta = graphMetaSchema.parse({
+    title: input.title,
+    language: input.language,
+  });
+  const graph = graphSchema.parse(input.graph);
+  const source = sourceSchema.parse(input.source);
+
+  const [row] = await db
+    .insert(graphs)
+    .values({
+      userId,
+      title: meta.title,
+      language: meta.language,
+      graph: JSON.stringify(graph),
+      source: JSON.stringify(source),
+    })
+    .returning({ id: graphs.id });
+
+  return { id: row.id };
+}
+
+export async function listGraphs() {
+  const userId = await requireUserId();
+  return db
+    .select({
+      id: graphs.id,
+      title: graphs.title,
+      language: graphs.language,
+      createdAt: graphs.createdAt,
+    })
+    .from(graphs)
+    .where(eq(graphs.userId, userId))
+    .orderBy(desc(graphs.createdAt));
+}
+
+export async function getGraph(id: string) {
+  const userId = await requireUserId();
+  // Ownership is enforced in the query: a graph that isn't the caller's simply
+  // isn't returned.
+  const [row] = await db
+    .select()
+    .from(graphs)
+    .where(and(eq(graphs.id, id), eq(graphs.userId, userId)))
+    .limit(1);
+  return row ?? null;
+}
+
+export async function deleteGraph(id: string): Promise<void> {
+  const userId = await requireUserId();
+  await db
+    .delete(graphs)
+    .where(and(eq(graphs.id, id), eq(graphs.userId, userId)));
+}
diff --git a/src/lib/testimonials.ts b/src/lib/testimonials.ts
new file mode 100644
index 0000000..939aa34
--- /dev/null
+++ b/src/lib/testimonials.ts
@@ -0,0 +1,42 @@
+"use server";
+
+import { desc, eq } from "drizzle-orm";
+import { auth } from "@/auth";
+import { db } from "@/db";
+import { testimonials, users } from "@/db/schema";
+import { testimonialSchema } from "./validation";
+
+export async function listTestimonials() {
+  return db
+    .select({
+      id: testimonials.id,
+      body: testimonials.body,
+      createdAt: testimonials.createdAt,
+      userName: users.name,
+      userImage: users.image,
+    })
+    .from(testimonials)
+    .innerJoin(users, eq(testimonials.userId, users.id))
+    .orderBy(desc(testimonials.createdAt))
+    .limit(50);
+}
+
+export type TestimonialFormState = { ok: boolean; error?: string };
+
+export async function postTestimonial(
+  _prev: TestimonialFormState,
+  formData: FormData,
+): Promise<TestimonialFormState> {
+  const session = await auth();
+  if (!session?.user?.id) return { ok: false, error: "UNAUTHORIZED" };
+
+  const parsed = testimonialSchema.safeParse({ body: formData.get("body") });
+  if (!parsed.success) return { ok: false, error: "INVALID" };
+
+  await db.insert(testimonials).values({
+    userId: session.user.id,
+    body: parsed.data.body,
+  });
+
+  return { ok: true };
+}
diff --git a/src/lib/validation.test.ts b/src/lib/validation.test.ts
new file mode 100644
index 0000000..fb5a676
--- /dev/null
+++ b/src/lib/validation.test.ts
@@ -0,0 +1,69 @@
+import { describe, expect, test } from "vitest";
+import {
+  graphMetaSchema,
+  graphSchema,
+  sourceSchema,
+  testimonialSchema,
+} from "./validation";
+
+describe("graphMetaSchema", () => {
+  test("rejects an empty title", () => {
+    const result = graphMetaSchema.safeParse({ title: "  ", language: "python" });
+    expect(result.success).toBe(false);
+  });
+
+  test("trims and accepts a valid meta", () => {
+    const result = graphMetaSchema.safeParse({
+      title: "  My graph  ",
+      language: "python",
+    });
+    expect(result.success).toBe(true);
+    if (result.success) expect(result.data.title).toBe("My graph");
+  });
+});
+
+describe("graphSchema", () => {
+  test("accepts a graph with the known optional fields", () => {
+    const result = graphSchema.safeParse({
+      nodes: [{ id: "a", label: "a", type: "function", file: "x.py" }],
+      edges: [{ source: "a", target: "b", kind: "calls", column: "id" }],
+    });
+    expect(result.success).toBe(true);
+  });
+
+  test("rejects when nodes is not an array", () => {
+    const result = graphSchema.safeParse({ nodes: "nope", edges: [] });
+    expect(result.success).toBe(false);
+  });
+});
+
+describe("sourceSchema", () => {
+  test("rejects when neither code nor files is provided", () => {
+    const result = sourceSchema.safeParse({});
+    expect(result.success).toBe(false);
+  });
+
+  test("accepts a code snippet", () => {
+    const result = sourceSchema.safeParse({ code: "print(1)" });
+    expect(result.success).toBe(true);
+  });
+
+  test("accepts a non-empty file list", () => {
+    const result = sourceSchema.safeParse({
+      files: [{ path: "a.py", content: "print(1)" }],
+    });
+    expect(result.success).toBe(true);
+  });
+});
+
+describe("testimonialSchema", () => {
+  test("rejects a too-short body", () => {
+    expect(testimonialSchema.safeParse({ body: "hi" }).success).toBe(false);
+  });
+
+  test("accepts and trims a valid body", () => {
+    const result = testimonialSchema.safeParse({ body: "  great tool  " });
+    expect(result.success).toBe(true);
+    if (result.success) expect(result.data.body).toBe("great tool");
+  });
+});
diff --git a/src/lib/validation.ts b/src/lib/validation.ts
new file mode 100644
index 0000000..a79e392
--- /dev/null
+++ b/src/lib/validation.ts
@@ -0,0 +1,45 @@
+import { z } from "zod";
+
+// Server Actions are a trust boundary: validate everything a client can send.
+// These schemas live in a plain module (not a "use server" file) so they can be
+// imported by both the actions and the tests.
+
+const MAX_SOURCE_BYTES = 2_000_000;
+
+export const graphMetaSchema = z.object({
+  title: z.string().trim().min(1).max(120),
+  language: z.string().trim().min(1).max(20),
+});
+
+// The graph is produced by our own analyzer, but a client could POST anything.
+// Validate the structural essentials and allow the known optional fields via
+// loose objects rather than re-declaring the whole type.
+export const graphSchema = z.object({
+  nodes: z
+    .array(z.object({ id: z.string(), label: z.string(), type: z.string() }).loose())
+    .max(5000),
+  edges: z
+    .array(
+      z.object({ source: z.string(), target: z.string(), kind: z.string() }).loose(),
+    )
+    .max(20000),
+});
+
+export const sourceSchema = z
+  .object({
+    code: z.string().max(MAX_SOURCE_BYTES).optional(),
+    files: z
+      .array(z.object({ path: z.string(), content: z.string() }))
+      .max(400)
+      .optional(),
+  })
+  .refine((v) => v.code !== undefined || (v.files?.length ?? 0) > 0, {
+    message: "Provide 'code' or 'files'.",
+  });
+
+export const testimonialSchema = z.object({
+  body: z.string().trim().min(3).max(1000),
+});
+
+export type GraphInput = z.infer<typeof graphSchema>;
+export type SourceInput = z.infer<typeof sourceSchema>;
diff --git a/src/proxy.ts b/src/proxy.ts
index a51a6b9..43336a3 100644
--- a/src/proxy.ts
+++ b/src/proxy.ts
@@ -13,7 +13,7 @@ function buildCsp(nonce: string): string {
     "form-action 'self'",
     "frame-ancestors 'none'",
     "object-src 'none'",
-    "img-src 'self' data:",
+    "img-src 'self' data: https://avatars.githubusercontent.com",
     "font-src 'self' data:",
     "connect-src 'self'",
     `script-src 'self' 'nonce-${nonce}'${isDev ? " 'unsafe-eval'" : ""}`,

From efd965432efbff11a1667413b7debf032219ddf8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20De=20Le=C3=B3n?= <deleonalonso77@gmail.com>
Date: Thu, 18 Jun 2026 12:07:18 -0600
Subject: [PATCH 2/2] chore: add generated Drizzle migration for initial schema

---
 drizzle/0000_colossal_hex.sql   |  57 +++++
 drizzle/meta/0000_snapshot.json | 389 ++++++++++++++++++++++++++++++++
 drizzle/meta/_journal.json      |  13 ++
 3 files changed, 459 insertions(+)
 create mode 100644 drizzle/0000_colossal_hex.sql
 create mode 100644 drizzle/meta/0000_snapshot.json
 create mode 100644 drizzle/meta/_journal.json

diff --git a/drizzle/0000_colossal_hex.sql b/drizzle/0000_colossal_hex.sql
new file mode 100644
index 0000000..1e862d8
--- /dev/null
+++ b/drizzle/0000_colossal_hex.sql
@@ -0,0 +1,57 @@
+CREATE TABLE `account` (
+	`userId` text NOT NULL,
+	`type` text NOT NULL,
+	`provider` text NOT NULL,
+	`providerAccountId` text NOT NULL,
+	`refresh_token` text,
+	`access_token` text,
+	`expires_at` integer,
+	`token_type` text,
+	`scope` text,
+	`id_token` text,
+	`session_state` text,
+	PRIMARY KEY(`provider`, `providerAccountId`),
+	FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE TABLE `graph` (
+	`id` text PRIMARY KEY NOT NULL,
+	`userId` text NOT NULL,
+	`title` text NOT NULL,
+	`language` text NOT NULL,
+	`source` text NOT NULL,
+	`graph` text NOT NULL,
+	`createdAt` integer NOT NULL,
+	FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE TABLE `session` (
+	`sessionToken` text PRIMARY KEY NOT NULL,
+	`userId` text NOT NULL,
+	`expires` integer NOT NULL,
+	FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE TABLE `testimonial` (
+	`id` text PRIMARY KEY NOT NULL,
+	`userId` text NOT NULL,
+	`body` text NOT NULL,
+	`createdAt` integer NOT NULL,
+	FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
+);
+--> statement-breakpoint
+CREATE TABLE `user` (
+	`id` text PRIMARY KEY NOT NULL,
+	`name` text,
+	`email` text,
+	`emailVerified` integer,
+	`image` text
+);
+--> statement-breakpoint
+CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
+CREATE TABLE `verificationToken` (
+	`identifier` text NOT NULL,
+	`token` text NOT NULL,
+	`expires` integer NOT NULL,
+	PRIMARY KEY(`identifier`, `token`)
+);
diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json
new file mode 100644
index 0000000..c609a86
--- /dev/null
+++ b/drizzle/meta/0000_snapshot.json
@@ -0,0 +1,389 @@
+{
+  "version": "6",
+  "dialect": "sqlite",
+  "id": "2e120bd5-bc9a-4568-8262-4df3e119bd74",
+  "prevId": "00000000-0000-0000-0000-000000000000",
+  "tables": {
+    "account": {
+      "name": "account",
+      "columns": {
+        "userId": {
+          "name": "userId",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "type": {
+          "name": "type",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "provider": {
+          "name": "provider",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "providerAccountId": {
+          "name": "providerAccountId",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "refresh_token": {
+          "name": "refresh_token",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "access_token": {
+          "name": "access_token",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "token_type": {
+          "name": "token_type",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "scope": {
+          "name": "scope",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "id_token": {
+          "name": "id_token",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "session_state": {
+          "name": "session_state",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "account_userId_user_id_fk": {
+          "name": "account_userId_user_id_fk",
+          "tableFrom": "account",
+          "tableTo": "user",
+          "columnsFrom": [
+            "userId"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {
+        "account_provider_providerAccountId_pk": {
+          "columns": [
+            "provider",
+            "providerAccountId"
+          ],
+          "name": "account_provider_providerAccountId_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "graph": {
+      "name": "graph",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "userId": {
+          "name": "userId",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "title": {
+          "name": "title",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "language": {
+          "name": "language",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "source": {
+          "name": "source",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "graph": {
+          "name": "graph",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "createdAt": {
+          "name": "createdAt",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "graph_userId_user_id_fk": {
+          "name": "graph_userId_user_id_fk",
+          "tableFrom": "graph",
+          "tableTo": "user",
+          "columnsFrom": [
+            "userId"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "session": {
+      "name": "session",
+      "columns": {
+        "sessionToken": {
+          "name": "sessionToken",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "userId": {
+          "name": "userId",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "expires": {
+          "name": "expires",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "session_userId_user_id_fk": {
+          "name": "session_userId_user_id_fk",
+          "tableFrom": "session",
+          "tableTo": "user",
+          "columnsFrom": [
+            "userId"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "testimonial": {
+      "name": "testimonial",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "userId": {
+          "name": "userId",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "body": {
+          "name": "body",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "createdAt": {
+          "name": "createdAt",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {
+        "testimonial_userId_user_id_fk": {
+          "name": "testimonial_userId_user_id_fk",
+          "tableFrom": "testimonial",
+          "tableTo": "user",
+          "columnsFrom": [
+            "userId"
+          ],
+          "columnsTo": [
+            "id"
+          ],
+          "onDelete": "cascade",
+          "onUpdate": "no action"
+        }
+      },
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "user": {
+      "name": "user",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "text",
+          "primaryKey": true,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "name": {
+          "name": "name",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "email": {
+          "name": "email",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "emailVerified": {
+          "name": "emailVerified",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        },
+        "image": {
+          "name": "image",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false,
+          "autoincrement": false
+        }
+      },
+      "indexes": {
+        "user_email_unique": {
+          "name": "user_email_unique",
+          "columns": [
+            "email"
+          ],
+          "isUnique": true
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    },
+    "verificationToken": {
+      "name": "verificationToken",
+      "columns": {
+        "identifier": {
+          "name": "identifier",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "token": {
+          "name": "token",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        },
+        "expires": {
+          "name": "expires",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "autoincrement": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {
+        "verificationToken_identifier_token_pk": {
+          "columns": [
+            "identifier",
+            "token"
+          ],
+          "name": "verificationToken_identifier_token_pk"
+        }
+      },
+      "uniqueConstraints": {},
+      "checkConstraints": {}
+    }
+  },
+  "views": {},
+  "enums": {},
+  "_meta": {
+    "schemas": {},
+    "tables": {},
+    "columns": {}
+  },
+  "internal": {
+    "indexes": {}
+  }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
new file mode 100644
index 0000000..14b7cf6
--- /dev/null
+++ b/drizzle/meta/_journal.json
@@ -0,0 +1,13 @@
+{
+  "version": "7",
+  "dialect": "sqlite",
+  "entries": [
+    {
+      "idx": 0,
+      "version": "6",
+      "when": 1781805681891,
+      "tag": "0000_colossal_hex",
+      "breakpoints": true
+    }
+  ]
+}
\ No newline at end of file
