From a322ee6a43f46affaf0fdb4cd3b85cf3080cfef2 Mon Sep 17 00:00:00 2001
From: Vinicius Garcia
Date: Thu, 8 Oct 2026 18:58:55 +0000
Subject: [PATCH] spike(auth): prova de conceito do Better Auth (nao mergear em
develop)
Co-Authored-By: Claude Sonnet 5.5
Claude-Session: https://claude.ai/code/session_01VDxbrneS3TEAUmdgVmkKpQ
---
.env.example | 12 +
SPIKE-BETTER-AUTH.md | 25 +
drizzle.config.ts | 2 +-
drizzle/0005_better_auth_spike.sql | 91 ++
drizzle/meta/0005_snapshot.json | 1937 ++++++++++++++++++++++++
drizzle/meta/_journal.json | 9 +-
package-lock.json | 464 +++++-
package.json | 1 +
src/app/api/auth/[...all]/route.ts | 6 +
src/app/spike/accept/[id]/accept.tsx | 25 +
src/app/spike/accept/[id]/page.tsx | 15 +
src/app/spike/layout.tsx | 6 +
src/app/spike/org-panel.tsx | 58 +
src/app/spike/page.tsx | 33 +
src/app/spike/sign-in/page.tsx | 5 +
src/app/spike/sign-in/sign-in-form.tsx | 60 +
src/app/spike/tenant.ts | 6 +
src/db/auth-schema.ts | 178 +++
src/lib/ba/auth-client.ts | 6 +
src/server/ba/auth.smoke.test.ts | 77 +
src/server/ba/auth.ts | 86 ++
src/server/ba/context.test.ts | 16 +
src/server/ba/context.ts | 54 +
src/server/ba/mail.ts | 17 +
24 files changed, 3142 insertions(+), 47 deletions(-)
create mode 100644 SPIKE-BETTER-AUTH.md
create mode 100644 drizzle/0005_better_auth_spike.sql
create mode 100644 drizzle/meta/0005_snapshot.json
create mode 100644 src/app/api/auth/[...all]/route.ts
create mode 100644 src/app/spike/accept/[id]/accept.tsx
create mode 100644 src/app/spike/accept/[id]/page.tsx
create mode 100644 src/app/spike/layout.tsx
create mode 100644 src/app/spike/org-panel.tsx
create mode 100644 src/app/spike/page.tsx
create mode 100644 src/app/spike/sign-in/page.tsx
create mode 100644 src/app/spike/sign-in/sign-in-form.tsx
create mode 100644 src/app/spike/tenant.ts
create mode 100644 src/db/auth-schema.ts
create mode 100644 src/lib/ba/auth-client.ts
create mode 100644 src/server/ba/auth.smoke.test.ts
create mode 100644 src/server/ba/auth.ts
create mode 100644 src/server/ba/context.test.ts
create mode 100644 src/server/ba/context.ts
create mode 100644 src/server/ba/mail.ts
diff --git a/.env.example b/.env.example
index 2f99090..c73255c 100644
--- a/.env.example
+++ b/.env.example
@@ -7,3 +7,15 @@ CLERK_SECRET_KEY=sk_test_xxxxxxxx
# Clerk webhook: Dashboard > Webhooks > your endpoint > Signing Secret (whsec_...)
CLERK_WEBHOOK_SIGNING_SECRET=whsec_xxxxxxxx
+
+# SPIKE (ADR-033, branch spike/better-auth only)
+BETTER_AUTH_SECRET=generate-with-openssl-rand-base64-32
+BETTER_AUTH_URL=http://localhost:3000
+# Optional: without RESEND_API_KEY the e-mails (OTP, invitation) are printed in the server console
+RESEND_API_KEY=
+MAIL_FROM=
+# Optional social login (redirect: http://localhost:3000/api/auth/callback/)
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
+GITHUB_CLIENT_ID=
+GITHUB_CLIENT_SECRET=
diff --git a/SPIKE-BETTER-AUTH.md b/SPIKE-BETTER-AUTH.md
new file mode 100644
index 0000000..c266cd9
--- /dev/null
+++ b/SPIKE-BETTER-AUTH.md
@@ -0,0 +1,25 @@
+# Spike: Better Auth (ADR-033) — NÃO MERGEAR EM `develop`
+
+Branch de investigação. Nada aqui vai para produção: o merge em `develop` faz deploy e a migration `0005_better_auth_spike` não deve rodar no Neon `production`.
+
+## O que foi montado
+- `src/db/auth-schema.ts` + `drizzle/0005_better_auth_spike.sql`: tabelas `ba_*` (user, session, account, verification, organization, member, invitation), geradas por `npx auth@latest generate` e só renomeadas com prefixo. Tabelas públicas do app intocadas.
+- `src/server/ba/auth.ts`: `createAuth()`/`getAuth()` — Drizzle adapter `pg`, `emailOTP`, `organization` (convite por e-mail), Google/GitHub condicionais por env, hook que ativa a primeira organização no login, `nextCookies()` por último.
+- `src/server/ba/context.ts`: `requireBaWorkspaceContext()` com o mesmo contrato de `requireWorkspaceContext()`; espelha em `users`/`workspaces` reutilizando os upserts de `clerk-sync.ts` (ids com prefixo `ba:` nas colunas `clerk_*`, para não migrar tabelas públicas no spike).
+- `src/app/api/auth/[...all]/route.ts`, `src/lib/ba/auth-client.ts`, páginas `/spike`, `/spike/sign-in`, `/spike/accept/[id]`.
+- Testes: `src/server/ba/auth.smoke.test.ts` (PGlite, Better Auth real) e `context.test.ts`.
+
+## Como testar localmente (banco `development` do Neon!)
+1. No `.env.local`: `BETTER_AUTH_SECRET` (`openssl rand -base64 32`), `BETTER_AUTH_URL=http://localhost:3000`; opcionais `RESEND_API_KEY`, `GOOGLE_*`, `GITHUB_*` (callback `http://localhost:3000/api/auth/callback/`).
+2. Conferir que `DATABASE_URL` aponta para `development`; `npm run db:migrate` (ou `npx drizzle-kit migrate`).
+3. `npm run dev`; abrir `/spike/sign-in`. Sem `RESEND_API_KEY`, o código e o link de convite saem no console do servidor.
+
+## Critérios (ADR-033)
+| | Critério | Estado |
+|---|---|---|
+| a | Login por código + Google + GitHub | Código: validado em teste (PGlite). Google/GitHub: configurado, **não testado** (precisa de credenciais OAuth) |
+| b | Funciona com neon-http | Pelo código do adapter, `pg` só usa transação com `transaction: true` (desligado) → provável OK. **Não testado contra Neon** |
+| c | Organizações ↔ workspaces, admin/member | Validado em teste (owner/admin → admin; member). Ponte `requireBaWorkspaceContext()` só typecheck + teste de papel; **rodar no app** |
+| d | Convite por e-mail aceito por 2º usuário | Fluxo validado em teste (inclui recusa de e-mail diferente). Envio real via Resend **não testado** |
+| e | Sessão lida em `getTenant()` sem mexer no resto | Contrato igual; páginas `/spike` usam a ponte. **Rodar no app** |
+| f | Estimativa de migração | Ver ADR-033 (atualizado após o teste manual) |
diff --git a/drizzle.config.ts b/drizzle.config.ts
index 0c5a6d4..0ba792b 100644
--- a/drizzle.config.ts
+++ b/drizzle.config.ts
@@ -8,7 +8,7 @@ try {
export default defineConfig({
dialect: "postgresql",
- schema: "./src/db/schema.ts",
+ schema: ["./src/db/schema.ts", "./src/db/auth-schema.ts"],
out: "./drizzle",
dbCredentials: { url: process.env.DATABASE_URL ?? "" },
strict: true,
diff --git a/drizzle/0005_better_auth_spike.sql b/drizzle/0005_better_auth_spike.sql
new file mode 100644
index 0000000..df65fb6
--- /dev/null
+++ b/drizzle/0005_better_auth_spike.sql
@@ -0,0 +1,91 @@
+CREATE TABLE "ba_account" (
+ "id" text PRIMARY KEY NOT NULL,
+ "account_id" text NOT NULL,
+ "provider_id" text NOT NULL,
+ "user_id" text NOT NULL,
+ "access_token" text,
+ "refresh_token" text,
+ "id_token" text,
+ "access_token_expires_at" timestamp,
+ "refresh_token_expires_at" timestamp,
+ "scope" text,
+ "password" text,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "ba_invitation" (
+ "id" text PRIMARY KEY NOT NULL,
+ "organization_id" text NOT NULL,
+ "email" text NOT NULL,
+ "role" text,
+ "status" text DEFAULT 'pending' NOT NULL,
+ "expires_at" timestamp NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "inviter_id" text NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "ba_member" (
+ "id" text PRIMARY KEY NOT NULL,
+ "organization_id" text NOT NULL,
+ "user_id" text NOT NULL,
+ "role" text DEFAULT 'member' NOT NULL,
+ "created_at" timestamp NOT NULL
+);
+--> statement-breakpoint
+CREATE TABLE "ba_organization" (
+ "id" text PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ "slug" text NOT NULL,
+ "logo" text,
+ "created_at" timestamp NOT NULL,
+ "metadata" text,
+ CONSTRAINT "ba_organization_slug_unique" UNIQUE("slug")
+);
+--> statement-breakpoint
+CREATE TABLE "ba_session" (
+ "id" text PRIMARY KEY NOT NULL,
+ "expires_at" timestamp NOT NULL,
+ "token" text NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp NOT NULL,
+ "ip_address" text,
+ "user_agent" text,
+ "user_id" text NOT NULL,
+ "active_organization_id" text,
+ CONSTRAINT "ba_session_token_unique" UNIQUE("token")
+);
+--> statement-breakpoint
+CREATE TABLE "ba_user" (
+ "id" text PRIMARY KEY NOT NULL,
+ "name" text NOT NULL,
+ "email" text NOT NULL,
+ "email_verified" boolean DEFAULT false NOT NULL,
+ "image" text,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL,
+ CONSTRAINT "ba_user_email_unique" UNIQUE("email")
+);
+--> statement-breakpoint
+CREATE TABLE "ba_verification" (
+ "id" text PRIMARY KEY NOT NULL,
+ "identifier" text NOT NULL,
+ "value" text NOT NULL,
+ "expires_at" timestamp NOT NULL,
+ "created_at" timestamp DEFAULT now() NOT NULL,
+ "updated_at" timestamp DEFAULT now() NOT NULL
+);
+--> statement-breakpoint
+ALTER TABLE "ba_account" ADD CONSTRAINT "ba_account_user_id_ba_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."ba_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "ba_invitation" ADD CONSTRAINT "ba_invitation_organization_id_ba_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."ba_organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "ba_invitation" ADD CONSTRAINT "ba_invitation_inviter_id_ba_user_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."ba_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "ba_member" ADD CONSTRAINT "ba_member_organization_id_ba_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."ba_organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "ba_member" ADD CONSTRAINT "ba_member_user_id_ba_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."ba_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+ALTER TABLE "ba_session" ADD CONSTRAINT "ba_session_user_id_ba_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."ba_user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "account_userId_idx" ON "ba_account" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "invitation_organizationId_idx" ON "ba_invitation" USING btree ("organization_id");--> statement-breakpoint
+CREATE INDEX "invitation_email_idx" ON "ba_invitation" USING btree ("email");--> statement-breakpoint
+CREATE INDEX "member_organizationId_idx" ON "ba_member" USING btree ("organization_id");--> statement-breakpoint
+CREATE INDEX "member_userId_idx" ON "ba_member" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "session_userId_idx" ON "ba_session" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "verification_identifier_idx" ON "ba_verification" USING btree ("identifier");
\ No newline at end of file
diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json
new file mode 100644
index 0000000..371c453
--- /dev/null
+++ b/drizzle/meta/0005_snapshot.json
@@ -0,0 +1,1937 @@
+{
+ "id": "3f76dd64-34e2-40da-9976-76d625a66599",
+ "prevId": "68e2ba09-141c-4b07-a6c4-c41e1428e55d",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.organizations": {
+ "name": "organizations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_archived": {
+ "name": "is_archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "organizations_ws_name_uq": {
+ "name": "organizations_ws_name_uq",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "organizations_workspace_id_workspaces_id_fk": {
+ "name": "organizations_workspace_id_workspaces_id_fk",
+ "tableFrom": "organizations",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.project_members": {
+ "name": "project_members",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "project_members_project_user_uq": {
+ "name": "project_members_project_user_uq",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "project_members_ws_user_idx": {
+ "name": "project_members_ws_user_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "project_members_workspace_id_workspaces_id_fk": {
+ "name": "project_members_workspace_id_workspaces_id_fk",
+ "tableFrom": "project_members",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "project_members_project_id_projects_id_fk": {
+ "name": "project_members_project_id_projects_id_fk",
+ "tableFrom": "project_members",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "project_members_user_id_users_id_fk": {
+ "name": "project_members_user_id_users_id_fk",
+ "tableFrom": "project_members",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.projects": {
+ "name": "projects",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#3B82F6'"
+ },
+ "is_archived": {
+ "name": "is_archived",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "projects_org_name_uq": {
+ "name": "projects_org_name_uq",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "projects_ws_idx": {
+ "name": "projects_ws_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "projects_workspace_id_workspaces_id_fk": {
+ "name": "projects_workspace_id_workspaces_id_fk",
+ "tableFrom": "projects",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "projects_organization_id_organizations_id_fk": {
+ "name": "projects_organization_id_organizations_id_fk",
+ "tableFrom": "projects",
+ "tableTo": "organizations",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tags": {
+ "name": "tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'#6B7280'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tags_ws_name_uq": {
+ "name": "tags_ws_name_uq",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tags_workspace_id_workspaces_id_fk": {
+ "name": "tags_workspace_id_workspaces_id_fk",
+ "tableFrom": "tags",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tasks": {
+ "name": "tasks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_completed": {
+ "name": "is_completed",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "tasks_project_name_uq": {
+ "name": "tasks_project_name_uq",
+ "columns": [
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "tasks_ws_idx": {
+ "name": "tasks_ws_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "tasks_workspace_id_workspaces_id_fk": {
+ "name": "tasks_workspace_id_workspaces_id_fk",
+ "tableFrom": "tasks",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "tasks_project_id_projects_id_fk": {
+ "name": "tasks_project_id_projects_id_fk",
+ "tableFrom": "tasks",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.time_entries": {
+ "name": "time_entries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "project_id": {
+ "name": "project_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "task_id": {
+ "name": "task_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "''"
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_billable": {
+ "name": "is_billable",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "time_entries_one_running_per_user_uq": {
+ "name": "time_entries_one_running_per_user_uq",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"time_entries\".\"ended_at\" is null and \"time_entries\".\"deleted_at\" is null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "time_entries_ws_user_start_idx": {
+ "name": "time_entries_ws_user_start_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "time_entries_ws_project_start_idx": {
+ "name": "time_entries_ws_project_start_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "project_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "time_entries_workspace_id_workspaces_id_fk": {
+ "name": "time_entries_workspace_id_workspaces_id_fk",
+ "tableFrom": "time_entries",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "time_entries_user_id_users_id_fk": {
+ "name": "time_entries_user_id_users_id_fk",
+ "tableFrom": "time_entries",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "time_entries_project_id_projects_id_fk": {
+ "name": "time_entries_project_id_projects_id_fk",
+ "tableFrom": "time_entries",
+ "tableTo": "projects",
+ "columnsFrom": [
+ "project_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "time_entries_task_id_tasks_id_fk": {
+ "name": "time_entries_task_id_tasks_id_fk",
+ "tableFrom": "time_entries",
+ "tableTo": "tasks",
+ "columnsFrom": [
+ "task_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "time_entries_end_after_start": {
+ "name": "time_entries_end_after_start",
+ "value": "\"time_entries\".\"ended_at\" is null or \"time_entries\".\"ended_at\" > \"time_entries\".\"started_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.time_entry_audit": {
+ "name": "time_entry_audit",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "time_entry_id": {
+ "name": "time_entry_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_user_id": {
+ "name": "actor_user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "changes": {
+ "name": "changes",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "time_entry_audit_entry_idx": {
+ "name": "time_entry_audit_entry_idx",
+ "columns": [
+ {
+ "expression": "time_entry_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "time_entry_audit_ws_idx": {
+ "name": "time_entry_audit_ws_idx",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "time_entry_audit_workspace_id_workspaces_id_fk": {
+ "name": "time_entry_audit_workspace_id_workspaces_id_fk",
+ "tableFrom": "time_entry_audit",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "time_entry_audit_time_entry_id_time_entries_id_fk": {
+ "name": "time_entry_audit_time_entry_id_time_entries_id_fk",
+ "tableFrom": "time_entry_audit",
+ "tableTo": "time_entries",
+ "columnsFrom": [
+ "time_entry_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "time_entry_audit_actor_user_id_users_id_fk": {
+ "name": "time_entry_audit_actor_user_id_users_id_fk",
+ "tableFrom": "time_entry_audit",
+ "tableTo": "users",
+ "columnsFrom": [
+ "actor_user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "restrict",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.time_entry_tags": {
+ "name": "time_entry_tags",
+ "schema": "",
+ "columns": {
+ "time_entry_id": {
+ "name": "time_entry_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "time_entry_tags_tag_idx": {
+ "name": "time_entry_tags_tag_idx",
+ "columns": [
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "time_entry_tags_time_entry_id_time_entries_id_fk": {
+ "name": "time_entry_tags_time_entry_id_time_entries_id_fk",
+ "tableFrom": "time_entry_tags",
+ "tableTo": "time_entries",
+ "columnsFrom": [
+ "time_entry_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "time_entry_tags_tag_id_tags_id_fk": {
+ "name": "time_entry_tags_tag_id_tags_id_fk",
+ "tableFrom": "time_entry_tags",
+ "tableTo": "tags",
+ "columnsFrom": [
+ "tag_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "time_entry_tags_time_entry_id_tag_id_pk": {
+ "name": "time_entry_tags_time_entry_id_tag_id_pk",
+ "columns": [
+ "time_entry_id",
+ "tag_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "clerk_id": {
+ "name": "clerk_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar_url": {
+ "name": "avatar_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "timezone": {
+ "name": "timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'America/Recife'"
+ },
+ "deleted_at": {
+ "name": "deleted_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_clerk_id_unique": {
+ "name": "users_clerk_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "clerk_id"
+ ]
+ },
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspace_members": {
+ "name": "workspace_members",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "workspace_id": {
+ "name": "workspace_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "workspace_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "removed_at": {
+ "name": "removed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "workspace_members_ws_user_uq": {
+ "name": "workspace_members_ws_user_uq",
+ "columns": [
+ {
+ "expression": "workspace_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "workspace_members_workspace_id_workspaces_id_fk": {
+ "name": "workspace_members_workspace_id_workspaces_id_fk",
+ "tableFrom": "workspace_members",
+ "tableTo": "workspaces",
+ "columnsFrom": [
+ "workspace_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "workspace_members_user_id_users_id_fk": {
+ "name": "workspace_members_user_id_users_id_fk",
+ "tableFrom": "workspace_members",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.workspaces": {
+ "name": "workspaces",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "uuid",
+ "primaryKey": true,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "clerk_org_id": {
+ "name": "clerk_org_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "report_timezone": {
+ "name": "report_timezone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "archived_at": {
+ "name": "archived_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "workspaces_clerk_org_id_unique": {
+ "name": "workspaces_clerk_org_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "clerk_org_id"
+ ]
+ },
+ "workspaces_slug_unique": {
+ "name": "workspaces_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ba_account": {
+ "name": "ba_account",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "account_userId_idx": {
+ "name": "account_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "ba_account_user_id_ba_user_id_fk": {
+ "name": "ba_account_user_id_ba_user_id_fk",
+ "tableFrom": "ba_account",
+ "tableTo": "ba_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ba_invitation": {
+ "name": "ba_invitation",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "inviter_id": {
+ "name": "inviter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "invitation_organizationId_idx": {
+ "name": "invitation_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "invitation_email_idx": {
+ "name": "invitation_email_idx",
+ "columns": [
+ {
+ "expression": "email",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "ba_invitation_organization_id_ba_organization_id_fk": {
+ "name": "ba_invitation_organization_id_ba_organization_id_fk",
+ "tableFrom": "ba_invitation",
+ "tableTo": "ba_organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "ba_invitation_inviter_id_ba_user_id_fk": {
+ "name": "ba_invitation_inviter_id_ba_user_id_fk",
+ "tableFrom": "ba_invitation",
+ "tableTo": "ba_user",
+ "columnsFrom": [
+ "inviter_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ba_member": {
+ "name": "ba_member",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'member'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "member_organizationId_idx": {
+ "name": "member_organizationId_idx",
+ "columns": [
+ {
+ "expression": "organization_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "member_userId_idx": {
+ "name": "member_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "ba_member_organization_id_ba_organization_id_fk": {
+ "name": "ba_member_organization_id_ba_organization_id_fk",
+ "tableFrom": "ba_member",
+ "tableTo": "ba_organization",
+ "columnsFrom": [
+ "organization_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "ba_member_user_id_ba_user_id_fk": {
+ "name": "ba_member_user_id_ba_user_id_fk",
+ "tableFrom": "ba_member",
+ "tableTo": "ba_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ba_organization": {
+ "name": "ba_organization",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "logo": {
+ "name": "logo",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "ba_organization_slug_unique": {
+ "name": "ba_organization_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "slug"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ba_session": {
+ "name": "ba_session",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active_organization_id": {
+ "name": "active_organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "session_userId_idx": {
+ "name": "session_userId_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "ba_session_user_id_ba_user_id_fk": {
+ "name": "ba_session_user_id_ba_user_id_fk",
+ "tableFrom": "ba_session",
+ "tableTo": "ba_user",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "ba_session_token_unique": {
+ "name": "ba_session_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ba_user": {
+ "name": "ba_user",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "image": {
+ "name": "image",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "ba_user_email_unique": {
+ "name": "ba_user_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ba_verification": {
+ "name": "ba_verification",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "verification_identifier_idx": {
+ "name": "verification_identifier_idx",
+ "columns": [
+ {
+ "expression": "identifier",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.workspace_role": {
+ "name": "workspace_role",
+ "schema": "public",
+ "values": [
+ "admin",
+ "member"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index c0c05b8..9cd8a2c 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -36,6 +36,13 @@
"when": 1791560000000,
"tag": "0004_time_entry_audit",
"breakpoints": true
+ },
+ {
+ "idx": 5,
+ "version": "7",
+ "when": 1791485860508,
+ "tag": "0005_better_auth_spike",
+ "breakpoints": true
}
]
-}
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 93cc65d..3ac8df3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
"@radix-ui/react-select": "^2.3.8",
"@radix-ui/react-slot": "^1.4.0",
"@react-pdf/renderer": "^4.9.0",
+ "better-auth": "^1.7.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.45.3",
@@ -306,6 +307,147 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@better-auth/core": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.7.7.tgz",
+ "integrity": "sha512-srgFzHEjB2WWlokFz1i4BfG41sNhsA105Pukgqu6RrEh5A+LSkzf1mCHydVipkjt9l5qswtBMi7IL6ouHbZL4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.41.1",
+ "@standard-schema/spec": "^1.1.0",
+ "zod": "^4.5.4"
+ },
+ "peerDependencies": {
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.2",
+ "@opentelemetry/api": "^1.9.0",
+ "better-call": "1.4.0",
+ "jose": "^6.1.0",
+ "kysely": "^0.28.5 || ^0.29.0",
+ "nanostores": "^1.0.1"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@better-auth/drizzle-adapter": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.7.7.tgz",
+ "integrity": "sha512-qon0U94PR5FQysuyGoAlGVQpIIiNg5dtDpwjE63sN/HjoRWZadZzHPcS9mn7JsJyQZK6iTNhd7LD0AaXt2f78w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0"
+ },
+ "peerDependenciesMeta": {
+ "drizzle-orm": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@better-auth/kysely-adapter": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.7.7.tgz",
+ "integrity": "sha512-9FONOCgOcrQI9wJ5v1oDGIg9sT7pa9RjZZQ4fsCTudo8R87SrIGNUvUxDFTJhZvUblvFfGaIBR9Vb8LA+8bqLQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "kysely": "^0.28.17 || ^0.29.0"
+ },
+ "peerDependenciesMeta": {
+ "kysely": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@better-auth/memory-adapter": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.7.7.tgz",
+ "integrity": "sha512-FqKFEe+b5tXXV0gRbyirca+qXgMpLOeJ8Wk19yykb/scQKy0WJ4k22WzEV3TTPctQV9DZhwCXPlDtQhnlOv5Gg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2"
+ }
+ },
+ "node_modules/@better-auth/mongo-adapter": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.7.7.tgz",
+ "integrity": "sha512-ZnVDuRrXqbf0oHphrEN27oTImNH5NTO26dtWatq5cXyYAsH7goof9QIoTOmxTbBxUQahKBw9T6+9h6n+RtOE+g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "mongodb": "^6.0.0 || ^7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "mongodb": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@better-auth/prisma-adapter": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.7.7.tgz",
+ "integrity": "sha512-4YcnrcVdvWiAZw0sZl63tWUClPXg5r3QsLn7C2sG7kKEBY06zOng0ibecyxRBixKq1zE1ceHuEUqez5Rhi+nQA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
+ "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@prisma/client": {
+ "optional": true
+ },
+ "prisma": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@better-auth/telemetry": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.7.7.tgz",
+ "integrity": "sha512-PaRA6i+kAioVYmza2gHPcdrxP0hAotph4KV9hft2GeNJq+AZXDiBu3FuLM9HxvcmiZeUT74Vufqd9ypmB8veTA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@better-auth/core": "^1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.2"
+ }
+ },
+ "node_modules/@better-auth/utils": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.2.tgz",
+ "integrity": "sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^2.0.1"
+ }
+ },
+ "node_modules/@better-auth/utils/node_modules/@noble/hashes": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
+ "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/@better-fetch/fetch": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.3.2.tgz",
+ "integrity": "sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==",
+ "license": "MIT"
+ },
"node_modules/@clerk/backend": {
"version": "3.23.0",
"resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-3.23.0.tgz",
@@ -389,7 +531,7 @@
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
"integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@electric-sql/pglite": {
@@ -437,7 +579,7 @@
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
"integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
"deprecated": "Merged into tsx: https://tsx.hirok.io",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.18.20",
@@ -822,7 +964,7 @@
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
- "dev": true,
+ "devOptional": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -861,7 +1003,7 @@
"resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
"integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
"deprecated": "Merged into tsx: https://tsx.hirok.io",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@esbuild-kit/core-utils": "^3.3.2",
@@ -2144,7 +2286,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -2154,14 +2296,14 @@
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
"integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -2448,11 +2590,20 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
+ "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@oxc-project/types": {
"version": "0.152.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz",
"integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peer": true,
"funding": {
@@ -3471,7 +3622,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peer": true
},
@@ -3488,6 +3639,12 @@
"integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==",
"license": "MIT"
},
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
"node_modules/@swc/helpers": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
@@ -3792,7 +3949,7 @@
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@types/deep-eql": "*",
@@ -3803,14 +3960,14 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
@@ -4480,7 +4637,7 @@
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz",
"integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "0.3.31",
@@ -4508,7 +4665,7 @@
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz",
"integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.6.0"
@@ -4518,7 +4675,7 @@
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz",
"integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"funding": {
"url": "https://opencollective.com/vitest"
@@ -4852,7 +5009,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -4953,6 +5110,172 @@
"node": ">=6.0.0"
}
},
+ "node_modules/better-auth": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.7.7.tgz",
+ "integrity": "sha512-Zhgxi/c5ylmfB/sX+nwnBbsFf/j6k0L8DvL6CJXZSrXgxc5pPMC/e7j812zNd4L4B4ELmdURCPw3X8nAOUdcjw==",
+ "license": "MIT",
+ "dependencies": {
+ "@better-auth/core": "1.7.7",
+ "@better-auth/drizzle-adapter": "1.7.7",
+ "@better-auth/kysely-adapter": "1.7.7",
+ "@better-auth/memory-adapter": "1.7.7",
+ "@better-auth/mongo-adapter": "1.7.7",
+ "@better-auth/prisma-adapter": "1.7.7",
+ "@better-auth/telemetry": "1.7.7",
+ "@better-auth/utils": "0.4.2",
+ "@better-fetch/fetch": "1.3.2",
+ "@noble/ciphers": "^2.2.0",
+ "@noble/hashes": "^2.2.0",
+ "better-call": "1.4.0",
+ "defu": "^6.1.4",
+ "jose": "^6.2.3",
+ "kysely": "^0.28.17 || ^0.29.0",
+ "nanostores": "^1.3.0",
+ "zod": "^4.5.4"
+ },
+ "peerDependencies": {
+ "@lynx-js/react": "*",
+ "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0",
+ "@sveltejs/kit": "^2.0.0",
+ "@tanstack/react-start": "^1.0.0",
+ "@tanstack/solid-start": "^1.0.0",
+ "drizzle-kit": ">=0.31.4 || >=1.0.0-beta.1",
+ "drizzle-orm": "^0.45.2 || >=1.0.0-rc.1 <2.0.0",
+ "mongodb": "^6.0.0 || ^7.0.0",
+ "mysql2": "^3.0.0",
+ "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
+ "pg": "^8.0.0",
+ "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0",
+ "solid-js": "^1.0.0",
+ "svelte": "^4.0.0 || ^5.0.0",
+ "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0",
+ "vue": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@lynx-js/react": {
+ "optional": true
+ },
+ "@prisma/client": {
+ "optional": true
+ },
+ "@sveltejs/kit": {
+ "optional": true
+ },
+ "@tanstack/react-start": {
+ "optional": true
+ },
+ "@tanstack/solid-start": {
+ "optional": true
+ },
+ "drizzle-kit": {
+ "optional": true
+ },
+ "drizzle-orm": {
+ "optional": true
+ },
+ "mongodb": {
+ "optional": true
+ },
+ "mysql2": {
+ "optional": true
+ },
+ "next": {
+ "optional": true
+ },
+ "pg": {
+ "optional": true
+ },
+ "prisma": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "solid-js": {
+ "optional": true
+ },
+ "svelte": {
+ "optional": true
+ },
+ "vitest": {
+ "optional": true
+ },
+ "vue": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/better-auth/node_modules/@noble/ciphers": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.4.0.tgz",
+ "integrity": "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/better-auth/node_modules/@noble/hashes": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
+ "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/better-call": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.4.0.tgz",
+ "integrity": "sha512-bBKOT4vv1kZLDgxVePdilk/Jwkn+dtRRsmi3DzHcDP+WnswyVl6dR59l2HEeP/0cB+bDoopASAesWDPIdd/zZA==",
+ "license": "MIT",
+ "dependencies": {
+ "@better-auth/utils": "^0.5.0",
+ "@better-fetch/fetch": "^1.3.1",
+ "rou3": "^0.9.1",
+ "set-cookie-parser": "^3.1.2"
+ },
+ "peerDependencies": {
+ "zod": "^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "zod": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/better-call/node_modules/@better-auth/utils": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.5.0.tgz",
+ "integrity": "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@noble/hashes": "^2.0.1"
+ }
+ },
+ "node_modules/better-call/node_modules/@noble/hashes": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.4.0.tgz",
+ "integrity": "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20.19.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
"node_modules/bidi-js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz",
@@ -5104,7 +5427,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/buffer-indexof-polyfill": {
@@ -5205,7 +5528,7 @@
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz",
"integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -5521,6 +5844,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/defu": {
+ "version": "6.1.7",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz",
+ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
+ "license": "MIT"
+ },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -5569,7 +5898,7 @@
"version": "0.31.11",
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.11.tgz",
"integrity": "sha512-YCYqxTLIB2OCqf6w9/Vef13baBVbwQIPcbT4Y56AfO6B9ajZhDhD8Jkveg/hJvT/iuMloKD/IYYkyMMNhr7Kqg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@drizzle-team/brocli": "^0.10.2",
@@ -6027,7 +6356,7 @@
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
- "dev": true,
+ "devOptional": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -6432,7 +6761,7 @@
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
"integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/es-object-atoms": {
@@ -6499,7 +6828,7 @@
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
- "dev": true,
+ "devOptional": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -6961,7 +7290,7 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0"
@@ -7010,7 +7339,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
"integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.0.0"
@@ -7367,7 +7696,7 @@
"version": "4.14.3",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz",
"integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
@@ -8148,6 +8477,15 @@
"jiti": "lib/jiti-cli.mjs"
}
},
+ "node_modules/jose": {
+ "version": "6.2.12",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz",
+ "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/panva"
+ }
+ },
"node_modules/js-cookie": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
@@ -8304,6 +8642,15 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/kysely": {
+ "version": "0.29.6",
+ "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.29.6.tgz",
+ "integrity": "sha512-hHaB8C/rfzDDtr/t8YZwxAuPJTT0zHyaPoVzcXwDYhYNAgH/4sIfVhi/XLLIY+bL/FqaIJnjATDbi8ObSELmxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
"node_modules/language-subtag-registry": {
"version": "0.3.23",
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
@@ -8927,6 +9274,21 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/nanostores": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.5.5.tgz",
+ "integrity": "sha512-FixtE239Gl6Gz0ZGCK0WzvNwObNCIwVT+L7zQcf1QgcA6AzBxblzqe9OT88qiucNkhsjSOqiogSaF5jVbJiqSA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": "^20.0.0 || >=22.0.0"
+ }
+ },
"node_modules/napi-postinstall": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
@@ -9183,7 +9545,7 @@
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
"integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
- "dev": true,
+ "devOptional": true,
"funding": [
"https://github.com/sponsors/sxzz",
"https://opencollective.com/debug"
@@ -9711,7 +10073,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
@@ -9751,7 +10113,7 @@
"version": "1.2.12",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz",
"integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -9782,6 +10144,12 @@
"@rolldown/binding-win32-x64-msvc": "1.2.12"
}
},
+ "node_modules/rou3": {
+ "version": "0.9.2",
+ "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.9.2.tgz",
+ "integrity": "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ==",
+ "license": "MIT"
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -9912,6 +10280,12 @@
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
"license": "MIT"
},
+ "node_modules/set-cookie-parser": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.3.tgz",
+ "integrity": "sha512-xletSdAI5efyVJjsatBbzDkXl3M/Zmad3xfnQe7ZXuBB0yNwW7C2WFsNGjdRNfZunmNHQuX79xs/ACMTLCdcew==",
+ "license": "MIT"
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -10126,7 +10500,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true,
+ "devOptional": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -10145,7 +10519,7 @@
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
@@ -10173,7 +10547,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
"integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/stop-iteration-iterator": {
@@ -10444,7 +10818,7 @@
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.1.tgz",
"integrity": "sha512-IAbQaPJIIdhVxi0mqy9lez1wEwje0UQf9F1vwtbelEG5Nbgn0nPOxCvU+mNeNS9gyJTAhrHp74CbUaSynQpZcQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=20.0.0"
@@ -10454,7 +10828,7 @@
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
"integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -10464,7 +10838,7 @@
"version": "0.2.17",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -10481,7 +10855,7 @@
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -10499,7 +10873,7 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -10598,7 +10972,7 @@
"version": "4.23.15",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz",
"integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.28.0"
@@ -11006,7 +11380,7 @@
"version": "8.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz",
"integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
@@ -11085,7 +11459,7 @@
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
"integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
- "dev": true,
+ "devOptional": true,
"license": "MPL-2.0",
"peer": true,
"dependencies": {
@@ -11358,7 +11732,7 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peer": true,
"engines": {
@@ -11372,7 +11746,7 @@
"version": "8.5.29",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz",
"integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==",
- "dev": true,
+ "devOptional": true,
"funding": [
{
"type": "opencollective",
@@ -11402,7 +11776,7 @@
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz",
"integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
@@ -11485,7 +11859,7 @@
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz",
"integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.6.0"
@@ -11495,7 +11869,7 @@
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
"integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -11609,7 +11983,7 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz",
"integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"bin": {
"why-is-node-running": "cli.js"
diff --git a/package.json b/package.json
index 68f31d8..a071487 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"@radix-ui/react-select": "^2.3.8",
"@radix-ui/react-slot": "^1.4.0",
"@react-pdf/renderer": "^4.9.0",
+ "better-auth": "^1.7.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.45.3",
diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts
new file mode 100644
index 0000000..e4462a8
--- /dev/null
+++ b/src/app/api/auth/[...all]/route.ts
@@ -0,0 +1,6 @@
+import { toNextJsHandler } from "better-auth/next-js";
+import { getAuth } from "@/server/ba/auth";
+
+// SPIKE (ADR-033): lazy so that importing the route never needs env vars.
+export const GET = (req: Request) => toNextJsHandler(getAuth()).GET(req);
+export const POST = (req: Request) => toNextJsHandler(getAuth()).POST(req);
diff --git a/src/app/spike/accept/[id]/accept.tsx b/src/app/spike/accept/[id]/accept.tsx
new file mode 100644
index 0000000..1683cef
--- /dev/null
+++ b/src/app/spike/accept/[id]/accept.tsx
@@ -0,0 +1,25 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { authClient } from "@/lib/ba/auth-client";
+
+export function AcceptInvitation({ id }: { id: string }) {
+ const [msg, setMsg] = useState(null);
+ return (
+
+
Aceitar convite
+
Entre com o MESMO e-mail do convite (em /spike/sign-in) e depois aceite aqui.
+
+ {msg ?
{msg}
: null}
+
+ );
+}
diff --git a/src/app/spike/accept/[id]/page.tsx b/src/app/spike/accept/[id]/page.tsx
new file mode 100644
index 0000000..f55b054
--- /dev/null
+++ b/src/app/spike/accept/[id]/page.tsx
@@ -0,0 +1,15 @@
+import { Suspense } from "react";
+import { AcceptInvitation } from "./accept";
+
+export default function AcceptPage({ params }: { params: Promise<{ id: string }> }) {
+ return (
+ Carregando…
}>
+
+
+ );
+}
+
+async function Inner({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ return ;
+}
diff --git a/src/app/spike/layout.tsx b/src/app/spike/layout.tsx
new file mode 100644
index 0000000..cd7d29c
--- /dev/null
+++ b/src/app/spike/layout.tsx
@@ -0,0 +1,6 @@
+// SPIKE (ADR-033): throwaway pages to exercise Better Auth. Never merged into `develop`.
+export const metadata = { title: "Spike Better Auth", robots: { index: false } };
+
+export default function SpikeLayout({ children }: { children: React.ReactNode }) {
+ return {children};
+}
diff --git a/src/app/spike/org-panel.tsx b/src/app/spike/org-panel.tsx
new file mode 100644
index 0000000..3930b96
--- /dev/null
+++ b/src/app/spike/org-panel.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { authClient } from "@/lib/ba/auth-client";
+
+export function OrgPanel({ orgs, activeId }: { orgs: { id: string; name: string }[]; activeId: string | null }) {
+ const [name, setName] = useState("");
+ const [inviteEmail, setInviteEmail] = useState("");
+ const [role, setRole] = useState<"member" | "admin">("member");
+ const [msg, setMsg] = useState(null);
+
+ const reload = () => window.location.reload();
+
+ async function createOrg(e: React.FormEvent) {
+ e.preventDefault();
+ const slug = `${name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-${Math.random().toString(36).slice(2, 6)}`;
+ const res = await authClient.organization.create({ name, slug });
+ if (res.error) return setMsg(res.error.message ?? "Erro");
+ await authClient.organization.setActive({ organizationId: res.data.id });
+ reload();
+ }
+
+ async function invite(e: React.FormEvent) {
+ e.preventDefault();
+ const res = await authClient.organization.inviteMember({ email: inviteEmail, role, organizationId: activeId ?? undefined });
+ setMsg(res.error ? (res.error.message ?? "Erro") : `Convite enviado para ${inviteEmail}`);
+ }
+
+ return (
+
+
+ {orgs.map((o) => (
+
+ ))}
+
+
+ {activeId ? (
+
+ ) : null}
+
+ {msg ?
{msg}
: null}
+
+ );
+}
diff --git a/src/app/spike/page.tsx b/src/app/spike/page.tsx
new file mode 100644
index 0000000..8bb85e4
--- /dev/null
+++ b/src/app/spike/page.tsx
@@ -0,0 +1,33 @@
+import { headers } from "next/headers";
+import { redirect } from "next/navigation";
+import { Suspense } from "react";
+import { getTenant } from "./tenant";
+import { getAuth } from "@/server/ba/auth";
+import { OrgPanel } from "./org-panel";
+
+export default function SpikePage() {
+ return (
+ Carregando…}>
+
+
+ );
+}
+
+async function SpikeContent() {
+ const session = await getAuth().api.getSession({ headers: await headers() });
+ if (!session) redirect("/spike/sign-in");
+ const orgs = await getAuth().api.listOrganizations({ headers: await headers() });
+ const activeId = session.session.activeOrganizationId ?? null;
+
+ // Criterion (e): the same contract the app uses today, resolved from the Better Auth session.
+ const ctx = activeId ? await getTenant().catch((e: unknown) => ({ error: String(e) })) : null;
+
+ return (
+
+
Spike Better Auth
+
Logado como {session.user.email}
+
({ id: o.id, name: o.name }))} activeId={activeId} />
+ {JSON.stringify(ctx, null, 2)}
+
+ );
+}
diff --git a/src/app/spike/sign-in/page.tsx b/src/app/spike/sign-in/page.tsx
new file mode 100644
index 0000000..5138ed1
--- /dev/null
+++ b/src/app/spike/sign-in/page.tsx
@@ -0,0 +1,5 @@
+import { SignInForm } from "./sign-in-form";
+
+export default function SpikeSignInPage() {
+ return ;
+}
diff --git a/src/app/spike/sign-in/sign-in-form.tsx b/src/app/spike/sign-in/sign-in-form.tsx
new file mode 100644
index 0000000..73e5793
--- /dev/null
+++ b/src/app/spike/sign-in/sign-in-form.tsx
@@ -0,0 +1,60 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { authClient } from "@/lib/ba/auth-client";
+
+export function SignInForm() {
+ const [email, setEmail] = useState("");
+ const [otp, setOtp] = useState("");
+ const [step, setStep] = useState<"email" | "code">("email");
+ const [error, setError] = useState(null);
+ const [busy, setBusy] = useState(false);
+
+ async function sendCode(e: React.FormEvent) {
+ e.preventDefault();
+ setBusy(true);
+ setError(null);
+ const res = await authClient.emailOtp.sendVerificationOtp({ email, type: "sign-in" });
+ setBusy(false);
+ if (res.error) setError(res.error.message ?? "Não foi possível enviar o código.");
+ else setStep("code");
+ }
+
+ async function verify(e: React.FormEvent) {
+ e.preventDefault();
+ setBusy(true);
+ setError(null);
+ const res = await authClient.signIn.emailOtp({ email, otp });
+ if (res.error) {
+ setBusy(false);
+ setError(res.error.message ?? "Código inválido.");
+ return;
+ }
+ window.location.assign("/spike");
+ }
+
+ return (
+
+
Entrar (spike Better Auth)
+ {step === "email" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+ {error ?
{error}
: null}
+
+ );
+}
diff --git a/src/app/spike/tenant.ts b/src/app/spike/tenant.ts
new file mode 100644
index 0000000..5a83d23
--- /dev/null
+++ b/src/app/spike/tenant.ts
@@ -0,0 +1,6 @@
+import { requireBaWorkspaceContext } from "@/server/ba/context";
+
+/** Criterion (e): proves the Clerk-free context plugs into the same shape `getTenant()` returns. */
+export async function getTenant() {
+ return requireBaWorkspaceContext();
+}
diff --git a/src/db/auth-schema.ts b/src/db/auth-schema.ts
new file mode 100644
index 0000000..5e3d1a8
--- /dev/null
+++ b/src/db/auth-schema.ts
@@ -0,0 +1,178 @@
+/**
+ * SPIKE (ADR-033): Better Auth tables, generated with `npx auth@latest generate` (better-auth 1.7.7, organization plugin)
+ * and only prefixed with `ba_` so they never collide with Compasso's own `users`/`workspaces`.
+ * Not part of `src/db/schema.ts` on purpose: the app schema stays untouched by the spike.
+ */
+import { relations } from "drizzle-orm";
+import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core";
+
+export const user = pgTable("ba_user", {
+ id: text("id").primaryKey(),
+ name: text("name").notNull(),
+ email: text("email").notNull().unique(),
+ emailVerified: boolean("email_verified").default(false).notNull(),
+ image: text("image"),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at")
+ .defaultNow()
+ .$onUpdate(() => /* @__PURE__ */ new Date())
+ .notNull(),
+});
+
+export const session = pgTable(
+ "ba_session",
+ {
+ id: text("id").primaryKey(),
+ expiresAt: timestamp("expires_at").notNull(),
+ token: text("token").notNull().unique(),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at")
+ .$onUpdate(() => /* @__PURE__ */ new Date())
+ .notNull(),
+ ipAddress: text("ip_address"),
+ userAgent: text("user_agent"),
+ userId: text("user_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ activeOrganizationId: text("active_organization_id"),
+ },
+ (table) => [index("session_userId_idx").on(table.userId)],
+);
+
+export const account = pgTable(
+ "ba_account",
+ {
+ id: text("id").primaryKey(),
+ accountId: text("account_id").notNull(),
+ providerId: text("provider_id").notNull(),
+ userId: text("user_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ accessToken: text("access_token"),
+ refreshToken: text("refresh_token"),
+ idToken: text("id_token"),
+ accessTokenExpiresAt: timestamp("access_token_expires_at"),
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
+ scope: text("scope"),
+ password: text("password"),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at")
+ .$onUpdate(() => /* @__PURE__ */ new Date())
+ .notNull(),
+ },
+ (table) => [index("account_userId_idx").on(table.userId)],
+);
+
+export const verification = pgTable(
+ "ba_verification",
+ {
+ id: text("id").primaryKey(),
+ identifier: text("identifier").notNull(),
+ value: text("value").notNull(),
+ expiresAt: timestamp("expires_at").notNull(),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ updatedAt: timestamp("updated_at")
+ .defaultNow()
+ .$onUpdate(() => /* @__PURE__ */ new Date())
+ .notNull(),
+ },
+ (table) => [index("verification_identifier_idx").on(table.identifier)],
+);
+
+export const organization = pgTable("ba_organization", {
+ id: text("id").primaryKey(),
+ name: text("name").notNull(),
+ slug: text("slug").notNull().unique(),
+ logo: text("logo"),
+ createdAt: timestamp("created_at").notNull(),
+ metadata: text("metadata"),
+});
+
+export const member = pgTable(
+ "ba_member",
+ {
+ id: text("id").primaryKey(),
+ organizationId: text("organization_id")
+ .notNull()
+ .references(() => organization.id, { onDelete: "cascade" }),
+ userId: text("user_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ role: text("role").default("member").notNull(),
+ createdAt: timestamp("created_at").notNull(),
+ },
+ (table) => [
+ index("member_organizationId_idx").on(table.organizationId),
+ index("member_userId_idx").on(table.userId),
+ ],
+);
+
+export const invitation = pgTable(
+ "ba_invitation",
+ {
+ id: text("id").primaryKey(),
+ organizationId: text("organization_id")
+ .notNull()
+ .references(() => organization.id, { onDelete: "cascade" }),
+ email: text("email").notNull(),
+ role: text("role"),
+ status: text("status").default("pending").notNull(),
+ expiresAt: timestamp("expires_at").notNull(),
+ createdAt: timestamp("created_at").defaultNow().notNull(),
+ inviterId: text("inviter_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ },
+ (table) => [
+ index("invitation_organizationId_idx").on(table.organizationId),
+ index("invitation_email_idx").on(table.email),
+ ],
+);
+
+export const userRelations = relations(user, ({ many }) => ({
+ sessions: many(session),
+ accounts: many(account),
+ members: many(member),
+ invitations: many(invitation),
+}));
+
+export const sessionRelations = relations(session, ({ one }) => ({
+ user: one(user, {
+ fields: [session.userId],
+ references: [user.id],
+ }),
+}));
+
+export const accountRelations = relations(account, ({ one }) => ({
+ user: one(user, {
+ fields: [account.userId],
+ references: [user.id],
+ }),
+}));
+
+export const organizationRelations = relations(organization, ({ many }) => ({
+ members: many(member),
+ invitations: many(invitation),
+}));
+
+export const memberRelations = relations(member, ({ one }) => ({
+ organization: one(organization, {
+ fields: [member.organizationId],
+ references: [organization.id],
+ }),
+ user: one(user, {
+ fields: [member.userId],
+ references: [user.id],
+ }),
+}));
+
+export const invitationRelations = relations(invitation, ({ one }) => ({
+ organization: one(organization, {
+ fields: [invitation.organizationId],
+ references: [organization.id],
+ }),
+ user: one(user, {
+ fields: [invitation.inviterId],
+ references: [user.id],
+ }),
+}));
diff --git a/src/lib/ba/auth-client.ts b/src/lib/ba/auth-client.ts
new file mode 100644
index 0000000..c4d9b74
--- /dev/null
+++ b/src/lib/ba/auth-client.ts
@@ -0,0 +1,6 @@
+"use client";
+
+import { emailOTPClient, organizationClient } from "better-auth/client/plugins";
+import { createAuthClient } from "better-auth/react";
+
+export const authClient = createAuthClient({ plugins: [emailOTPClient(), organizationClient()] });
diff --git a/src/server/ba/auth.smoke.test.ts b/src/server/ba/auth.smoke.test.ts
new file mode 100644
index 0000000..504a965
--- /dev/null
+++ b/src/server/ba/auth.smoke.test.ts
@@ -0,0 +1,77 @@
+import { beforeAll, describe, expect, it } from "vitest";
+import { createTestDb, type TestDb } from "@/test/db";
+import { createAuth } from "./auth";
+
+/**
+ * SPIKE (ADR-033): exercises the real Better Auth (email OTP, organization, invitations) against PGlite with the
+ * real migrations. It proves the logic and the schema; it does NOT prove the neon-http driver (run the app for that).
+ */
+describe("better auth smoke", () => {
+ let db: TestDb;
+ let auth: ReturnType;
+ const mails: { to: string; subject: string; text: string }[] = [];
+
+ beforeAll(async () => {
+ process.env.BETTER_AUTH_SECRET = "test-secret-test-secret-test-secret-123";
+ db = await createTestDb();
+ auth = createAuth({
+ db: db as never,
+ nextJsCookies: false,
+ send: async (m) => {
+ mails.push(m);
+ },
+ });
+ });
+
+ async function signIn(email: string) {
+ mails.length = 0;
+ await auth.api.sendVerificationOTP({ body: { email, type: "sign-in" } });
+ await new Promise((r) => setTimeout(r, 20)); // the OTP mail is intentionally fire-and-forget
+ const otp = /(\d{6})/.exec(mails[0].text)![1];
+ const res = await auth.api.signInEmailOTP({ body: { email, otp }, returnHeaders: true });
+ const cookie = res.headers
+ .getSetCookie()
+ .map((c) => c.split(";")[0])
+ .join("; ");
+ return { headers: new Headers({ cookie }), user: res.response.user };
+ }
+
+ it("signs in with an e-mail code, creates an organization and invites a second user who joins", async () => {
+ const owner = await signIn("owner@example.com");
+ const org = await auth.api.createOrganization({ headers: owner.headers, body: { name: "Acme", slug: "acme" } });
+ await auth.api.setActiveOrganization({ headers: owner.headers, body: { organizationId: org.id } });
+
+ mails.length = 0;
+ const invitation = await auth.api.createInvitation({
+ headers: owner.headers,
+ body: { email: "guest@example.com", role: "member", organizationId: org.id },
+ });
+ expect(mails.at(-1)?.text).toContain(`/spike/accept/${invitation.id}`);
+
+ const guest = await signIn("guest@example.com");
+ await auth.api.acceptInvitation({ headers: guest.headers, body: { invitationId: invitation.id } });
+ const members = await auth.api.listMembers({ headers: owner.headers, query: { organizationId: org.id } });
+ expect(members.members.map((m) => m.role).sort()).toEqual(["member", "owner"]);
+
+ // the session hook: a returning user lands in their first organization
+ const again = await signIn("owner@example.com");
+ const session = await auth.api.getSession({ headers: again.headers });
+ expect(session?.session.activeOrganizationId).toBe(org.id);
+ });
+
+ it("does not let another e-mail accept someone else's invitation", async () => {
+ const owner = await signIn("owner2@example.com");
+ const org = await auth.api.createOrganization({ headers: owner.headers, body: { name: "Beta", slug: "beta" } });
+ const invitation = await auth.api.createInvitation({
+ headers: owner.headers,
+ body: { email: "right@example.com", role: "member", organizationId: org.id },
+ });
+ const wrong = await signIn("wrong@example.com");
+ await expect(auth.api.acceptInvitation({ headers: wrong.headers, body: { invitationId: invitation.id } })).rejects.toThrow();
+ });
+
+ it("rejects a wrong code", async () => {
+ await auth.api.sendVerificationOTP({ body: { email: "x@example.com", type: "sign-in" } });
+ await expect(auth.api.signInEmailOTP({ body: { email: "x@example.com", otp: "000000" } })).rejects.toThrow();
+ });
+});
diff --git a/src/server/ba/auth.ts b/src/server/ba/auth.ts
new file mode 100644
index 0000000..136647c
--- /dev/null
+++ b/src/server/ba/auth.ts
@@ -0,0 +1,86 @@
+import { betterAuth } from "better-auth";
+import { drizzleAdapter } from "better-auth/adapters/drizzle";
+import { nextCookies } from "better-auth/next-js";
+import { emailOTP, organization } from "better-auth/plugins";
+import { asc, eq } from "drizzle-orm";
+import { getDb } from "@/db";
+import * as authSchema from "@/db/auth-schema";
+import { sendMail } from "./mail";
+
+type AuthDb = Parameters[0];
+type Mailer = (message: { to: string; subject: string; text: string }) => Promise;
+
+/** Factory so tests can inject an in-memory database and capture e-mails; the app uses `getAuth()`. */
+export function createAuth({
+ db,
+ send = sendMail,
+ nextJsCookies = true,
+}: {
+ db: AuthDb & ReturnType;
+ send?: Mailer;
+ nextJsCookies?: boolean;
+}) {
+ const baseURL = process.env.BETTER_AUTH_URL ?? "http://localhost:3000";
+ const google =
+ process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
+ ? { google: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET } }
+ : {};
+ const github =
+ process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET
+ ? { github: { clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET } }
+ : {};
+
+ return betterAuth({
+ baseURL,
+ secret: process.env.BETTER_AUTH_SECRET,
+ // neon-http (ADR-003): for provider "pg" the adapter only opens transactions when `transaction: true`, which we leave off.
+ database: drizzleAdapter(db, { provider: "pg", schema: authSchema }),
+ socialProviders: { ...google, ...github },
+ databaseHooks: {
+ session: {
+ create: {
+ // Sign in lands directly in the user's first organization (Clerk did this with the "active org").
+ before: async (session) => {
+ const [first] = await db
+ .select({ organizationId: authSchema.member.organizationId })
+ .from(authSchema.member)
+ .where(eq(authSchema.member.userId, session.userId))
+ .orderBy(asc(authSchema.member.createdAt))
+ .limit(1);
+ return { data: { ...session, activeOrganizationId: first?.organizationId ?? null } };
+ },
+ },
+ },
+ },
+ plugins: [
+ emailOTP({
+ otpLength: 6,
+ expiresIn: 300,
+ allowedAttempts: 3,
+ async sendVerificationOTP({ email, otp }) {
+ // Not awaited on purpose (timing attacks); errors are only logged. On Vercel use `after()` if delivery gets cut.
+ void send({ to: email, subject: "Seu código de acesso ao Compasso", text: `Seu código: ${otp}\nVálido por 5 minutos.` }).catch(
+ (e) => console.error("[spike mail] failed", e),
+ );
+ },
+ }),
+ organization({
+ async sendInvitationEmail({ id, email, organization: org, inviter }) {
+ await send({
+ to: email,
+ subject: `${inviter.user.name} convidou você para ${org.name} no Compasso`,
+ text: `Aceite o convite: ${baseURL}/spike/accept/${id}\n(expira em 48 horas)`,
+ });
+ },
+ }),
+ ...(nextJsCookies ? [nextCookies()] : []), // must be the last plugin
+ ],
+ });
+}
+
+let instance: ReturnType | undefined;
+
+/** Lazy singleton, like getDb(): `next build` must not need secrets. */
+export function getAuth() {
+ return (instance ??= createAuth({ db: getDb() }));
+}
diff --git a/src/server/ba/context.test.ts b/src/server/ba/context.test.ts
new file mode 100644
index 0000000..3884422
--- /dev/null
+++ b/src/server/ba/context.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from "vitest";
+import { roleFromBetterAuth } from "./context";
+
+describe("roleFromBetterAuth", () => {
+ it("treats owner and admin as workspace admin", () => {
+ expect(roleFromBetterAuth("owner")).toBe("admin");
+ expect(roleFromBetterAuth("admin")).toBe("admin");
+ expect(roleFromBetterAuth("member,admin")).toBe("admin");
+ });
+ it("treats member, unknown and empty roles as member", () => {
+ expect(roleFromBetterAuth("member")).toBe("member");
+ expect(roleFromBetterAuth("viewer")).toBe("member");
+ expect(roleFromBetterAuth(null)).toBe("member");
+ expect(roleFromBetterAuth(undefined)).toBe("member");
+ });
+});
diff --git a/src/server/ba/context.ts b/src/server/ba/context.ts
new file mode 100644
index 0000000..d58e58e
--- /dev/null
+++ b/src/server/ba/context.ts
@@ -0,0 +1,54 @@
+import { and, eq } from "drizzle-orm";
+import { headers } from "next/headers";
+import { redirect } from "next/navigation";
+import { getDb } from "@/db";
+import * as authSchema from "@/db/auth-schema";
+import { slugify } from "@/lib/slug";
+import { upsertMembership, upsertUser, upsertWorkspace } from "@/server/clerk-sync";
+import type { WorkspaceContext } from "@/server/workspace-context";
+import type { WorkspaceRole } from "@/lib/roles";
+import { getAuth } from "./auth";
+
+/**
+ * SPIKE (ADR-033): same contract as `requireWorkspaceContext()` (Clerk), fed by Better Auth.
+ * To avoid migrating public tables during the spike, Better Auth ids are mirrored into the existing
+ * `users.clerk_id` / `workspaces.clerk_org_id` columns with a "ba:" prefix. A real migration would rename them.
+ */
+export function roleFromBetterAuth(role: string | null | undefined): WorkspaceRole {
+ // The plugin stores comma-separated roles; owner and admin both administer the workspace.
+ const roles = (role ?? "").split(",").map((r) => r.trim());
+ return roles.includes("owner") || roles.includes("admin") ? "admin" : "member";
+}
+
+export async function requireBaWorkspaceContext(): Promise {
+ const session = await getAuth().api.getSession({ headers: await headers() });
+ if (!session) redirect("/spike/sign-in");
+ const orgId = session.session.activeOrganizationId;
+ if (!orgId) redirect("/spike");
+
+ const db = getDb();
+ const [row] = await db
+ .select({ role: authSchema.member.role, orgName: authSchema.organization.name, orgSlug: authSchema.organization.slug })
+ .from(authSchema.member)
+ .innerJoin(authSchema.organization, eq(authSchema.organization.id, authSchema.member.organizationId))
+ .where(and(eq(authSchema.member.userId, session.user.id), eq(authSchema.member.organizationId, orgId)))
+ .limit(1);
+ if (!row) redirect("/spike"); // stale active organization: the user is no longer a member
+
+ // Same idempotent upserts as the Clerk flow (neon-http, no transactions).
+ const user = await upsertUser({
+ clerkId: `ba:${session.user.id}`,
+ email: session.user.email,
+ name: session.user.name || null,
+ avatarUrl: session.user.image ?? null,
+ });
+ if (!user) throw new Error("User was deleted");
+ const workspace = await upsertWorkspace(
+ { clerkOrgId: `ba:${orgId}`, name: row.orgName, slug: `${slugify(row.orgSlug, 40)}-${orgId.slice(-6).toLowerCase()}` },
+ { unarchive: true },
+ );
+ const role = roleFromBetterAuth(row.role);
+ await upsertMembership(workspace.id, user.id, role);
+
+ return { userId: user.id, workspaceId: workspace.id, role, timezone: user.timezone, userName: user.name, workspaceName: workspace.name };
+}
diff --git a/src/server/ba/mail.ts b/src/server/ba/mail.ts
new file mode 100644
index 0000000..7068bf7
--- /dev/null
+++ b/src/server/ba/mail.ts
@@ -0,0 +1,17 @@
+/**
+ * SPIKE: minimal transactional mail. With RESEND_API_KEY it posts to the Resend REST API (no SDK dependency);
+ * without it the message is logged to the server console, which is enough to test the flows locally.
+ */
+export async function sendMail(message: { to: string; subject: string; text: string }) {
+ const apiKey = process.env.RESEND_API_KEY;
+ if (!apiKey) {
+ console.info(`[spike mail] to=${message.to} subject="${message.subject}"\n${message.text}`);
+ return;
+ }
+ const res = await fetch("https://api.resend.com/emails", {
+ method: "POST",
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
+ body: JSON.stringify({ from: process.env.MAIL_FROM ?? "Compasso ", ...message }),
+ });
+ if (!res.ok) throw new Error(`Resend responded ${res.status}`);
+}