qualia-framework 6.8.1 → 6.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/bin/install.js +159 -9
- package/bin/state.js +70 -5
- package/docs/EMPLOYEE-QUICKSTART.md +162 -0
- package/package.json +2 -1
- package/skills/qualia-doctor/SKILL.md +62 -0
- package/skills/qualia-new/REFERENCE.md +7 -0
- package/skills/qualia-new/SKILL.md +42 -0
- package/skills/qualia-report/SKILL.md +13 -0
- package/templates/stacks/README.md +110 -0
- package/templates/stacks/ai-agent/env.required.json +44 -0
- package/templates/stacks/ai-agent/phases.md +53 -0
- package/templates/stacks/ai-agent/scaffold/.env.example +12 -0
- package/templates/stacks/ai-agent/scaffold/README.md +31 -0
- package/templates/stacks/ai-agent/scaffold/app/api/chat/route.ts +33 -0
- package/templates/stacks/ai-agent/scaffold/app/globals.css +13 -0
- package/templates/stacks/ai-agent/scaffold/app/layout.tsx +19 -0
- package/templates/stacks/ai-agent/scaffold/app/page.tsx +12 -0
- package/templates/stacks/ai-agent/scaffold/evals/cases.json +23 -0
- package/templates/stacks/ai-agent/scaffold/lib/openrouter/client.ts +51 -0
- package/templates/stacks/ai-agent/scaffold/lib/prompts/system.ts +6 -0
- package/templates/stacks/ai-agent/scaffold/lib/supabase/client.ts +10 -0
- package/templates/stacks/ai-agent/scaffold/lib/supabase/server.ts +28 -0
- package/templates/stacks/ai-agent/scaffold/next.config.mjs +7 -0
- package/templates/stacks/ai-agent/scaffold/package.json +29 -0
- package/templates/stacks/ai-agent/scaffold/postcss.config.mjs +7 -0
- package/templates/stacks/ai-agent/scaffold/supabase/migrations/0001_init.sql +41 -0
- package/templates/stacks/ai-agent/scaffold/tsconfig.json +21 -0
- package/templates/stacks/ai-agent/stack.json +9 -0
- package/templates/stacks/ai-agent/verify-checklist.md +31 -0
- package/templates/stacks/full-app/env.required.json +20 -0
- package/templates/stacks/full-app/phases.md +45 -0
- package/templates/stacks/full-app/scaffold/.env.example +7 -0
- package/templates/stacks/full-app/scaffold/README.md +28 -0
- package/templates/stacks/full-app/scaffold/app/globals.css +14 -0
- package/templates/stacks/full-app/scaffold/app/layout.tsx +19 -0
- package/templates/stacks/full-app/scaffold/app/page.tsx +20 -0
- package/templates/stacks/full-app/scaffold/lib/supabase/client.ts +10 -0
- package/templates/stacks/full-app/scaffold/lib/supabase/server.ts +31 -0
- package/templates/stacks/full-app/scaffold/next.config.mjs +7 -0
- package/templates/stacks/full-app/scaffold/package.json +29 -0
- package/templates/stacks/full-app/scaffold/postcss.config.mjs +7 -0
- package/templates/stacks/full-app/scaffold/supabase/migrations/0001_init.sql +27 -0
- package/templates/stacks/full-app/scaffold/tsconfig.json +21 -0
- package/templates/stacks/full-app/stack.json +9 -0
- package/templates/stacks/full-app/verify-checklist.md +32 -0
- package/templates/stacks/internal-tool/env.required.json +20 -0
- package/templates/stacks/internal-tool/phases.md +45 -0
- package/templates/stacks/internal-tool/scaffold/.env.example +7 -0
- package/templates/stacks/internal-tool/scaffold/README.md +29 -0
- package/templates/stacks/internal-tool/scaffold/app/globals.css +13 -0
- package/templates/stacks/internal-tool/scaffold/app/layout.tsx +20 -0
- package/templates/stacks/internal-tool/scaffold/app/page.tsx +22 -0
- package/templates/stacks/internal-tool/scaffold/lib/supabase/client.ts +9 -0
- package/templates/stacks/internal-tool/scaffold/lib/supabase/server.ts +28 -0
- package/templates/stacks/internal-tool/scaffold/next.config.mjs +6 -0
- package/templates/stacks/internal-tool/scaffold/package.json +29 -0
- package/templates/stacks/internal-tool/scaffold/postcss.config.mjs +7 -0
- package/templates/stacks/internal-tool/scaffold/supabase/migrations/0001_init.sql +28 -0
- package/templates/stacks/internal-tool/scaffold/tsconfig.json +21 -0
- package/templates/stacks/internal-tool/stack.json +9 -0
- package/templates/stacks/internal-tool/verify-checklist.md +31 -0
- package/templates/stacks/landing-page/env.required.json +8 -0
- package/templates/stacks/landing-page/phases.md +42 -0
- package/templates/stacks/landing-page/scaffold/.env.example +3 -0
- package/templates/stacks/landing-page/scaffold/README.md +25 -0
- package/templates/stacks/landing-page/scaffold/app/globals.css +14 -0
- package/templates/stacks/landing-page/scaffold/app/layout.tsx +19 -0
- package/templates/stacks/landing-page/scaffold/app/page.tsx +21 -0
- package/templates/stacks/landing-page/scaffold/next.config.mjs +7 -0
- package/templates/stacks/landing-page/scaffold/package.json +26 -0
- package/templates/stacks/landing-page/scaffold/postcss.config.mjs +7 -0
- package/templates/stacks/landing-page/scaffold/tsconfig.json +21 -0
- package/templates/stacks/landing-page/stack.json +9 -0
- package/templates/stacks/landing-page/verify-checklist.md +28 -0
- package/tests/bin.test.sh +3 -3
- package/tests/state.test.sh +83 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { createServerClient } from "@supabase/ssr";
|
|
2
|
+
import { cookies } from "next/headers";
|
|
3
|
+
|
|
4
|
+
// Server-side Supabase adapter — all mutations and RLS-protected reads.
|
|
5
|
+
export async function createClient() {
|
|
6
|
+
const cookieStore = await cookies();
|
|
7
|
+
|
|
8
|
+
return createServerClient(
|
|
9
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
10
|
+
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
|
|
11
|
+
{
|
|
12
|
+
cookies: {
|
|
13
|
+
getAll() {
|
|
14
|
+
return cookieStore.getAll();
|
|
15
|
+
},
|
|
16
|
+
setAll(cookiesToSet) {
|
|
17
|
+
try {
|
|
18
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
19
|
+
cookieStore.set(name, value, options)
|
|
20
|
+
);
|
|
21
|
+
} catch {
|
|
22
|
+
// Server Component context — middleware refreshes the session.
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "next lint",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"next": "^16.0.0",
|
|
14
|
+
"react": "^19.0.0",
|
|
15
|
+
"react-dom": "^19.0.0",
|
|
16
|
+
"@supabase/supabase-js": "^2.45.0",
|
|
17
|
+
"@supabase/ssr": "^0.5.0",
|
|
18
|
+
"zod": "^3.23.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"@types/react": "^19.0.0",
|
|
23
|
+
"@types/react-dom": "^19.0.0",
|
|
24
|
+
"tailwindcss": "^4.0.0",
|
|
25
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
26
|
+
"postcss": "^8.4.0",
|
|
27
|
+
"typescript": "^5.6.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
-- Initial migration. RLS on every table from the first migration.
|
|
2
|
+
-- conversations + messages keyed to the auth user. Add pgvector tables in
|
|
3
|
+
-- Phase 1 if the agent needs RAG.
|
|
4
|
+
|
|
5
|
+
create table if not exists public.conversations (
|
|
6
|
+
id uuid primary key default gen_random_uuid(),
|
|
7
|
+
user_id uuid not null references auth.users (id) on delete cascade,
|
|
8
|
+
title text,
|
|
9
|
+
created_at timestamptz not null default now()
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
create table if not exists public.messages (
|
|
13
|
+
id uuid primary key default gen_random_uuid(),
|
|
14
|
+
conversation_id uuid not null references public.conversations (id) on delete cascade,
|
|
15
|
+
role text not null check (role in ('user', 'assistant', 'system')),
|
|
16
|
+
content text not null,
|
|
17
|
+
created_at timestamptz not null default now()
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
alter table public.conversations enable row level security;
|
|
21
|
+
alter table public.messages enable row level security;
|
|
22
|
+
|
|
23
|
+
create policy "conversations_own"
|
|
24
|
+
on public.conversations for all
|
|
25
|
+
using (auth.uid() = user_id)
|
|
26
|
+
with check (auth.uid() = user_id);
|
|
27
|
+
|
|
28
|
+
create policy "messages_own"
|
|
29
|
+
on public.messages for all
|
|
30
|
+
using (
|
|
31
|
+
exists (
|
|
32
|
+
select 1 from public.conversations c
|
|
33
|
+
where c.id = messages.conversation_id and c.user_id = auth.uid()
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
with check (
|
|
37
|
+
exists (
|
|
38
|
+
select 1 from public.conversations c
|
|
39
|
+
where c.id = messages.conversation_id and c.user_id = auth.uid()
|
|
40
|
+
)
|
|
41
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "ES2022"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": { "@/*": ["./*"] }
|
|
18
|
+
},
|
|
19
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
20
|
+
"exclude": ["node_modules"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "ai-agent",
|
|
3
|
+
"title": "AI Agent",
|
|
4
|
+
"description": "LLM / chat / agent product — Supabase data + OpenRouter routing, optional Retell/ElevenLabs/Telnyx voice. Ships only when the eval suite is green.",
|
|
5
|
+
"archetype": "ai-agent",
|
|
6
|
+
"defaultStack": "Next.js 16 (Vercel) · Supabase (Postgres + pgvector) · OpenRouter · Tailwind · Railway (workers, optional)",
|
|
7
|
+
"scaffoldEntry": "app/api/chat/route.ts",
|
|
8
|
+
"whenToUse": "Chat, RAG, tool-using agents, or voice agents. Pick this when an LLM is the core of the product — it adds OpenRouter routing, eval gates, and cost guardrails on top of a full app."
|
|
9
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# AI Agent — Definition of Done
|
|
2
|
+
|
|
3
|
+
Per-stack verify checklist. Inherits `rules/constitution.md` and the `ai-agent` archetype DoD.
|
|
4
|
+
|
|
5
|
+
## Foundation & data
|
|
6
|
+
- [ ] Supabase with **RLS on every table** (conversations, messages, users, embeddings); pgvector if RAG.
|
|
7
|
+
- [ ] Migrations in version control; RLS verified as two+ users.
|
|
8
|
+
|
|
9
|
+
## Agent core
|
|
10
|
+
- [ ] LLM via **OpenRouter** with model fallback — never a hardcoded single provider.
|
|
11
|
+
- [ ] System prompts versioned in source, never inline.
|
|
12
|
+
- [ ] Streaming responses; context-window management.
|
|
13
|
+
|
|
14
|
+
## Evals (THE SHIP GATE)
|
|
15
|
+
- [ ] Pass/fail eval suite over real cases — green before "done".
|
|
16
|
+
- [ ] Covers the success metric AND the refusal/guardrail cases.
|
|
17
|
+
|
|
18
|
+
## Guardrails & cost
|
|
19
|
+
- [ ] Input validation; refusal/safety behavior; graceful fallback on model failure.
|
|
20
|
+
- [ ] Per-request + daily cost ceilings; token + latency logging.
|
|
21
|
+
- [ ] Each tool/action validated server-side; timeout + failure handling; idempotency on writes.
|
|
22
|
+
|
|
23
|
+
## Voice (if applicable)
|
|
24
|
+
- [ ] Latency budget < 800ms end-to-end through Retell + ElevenLabs + Telnyx.
|
|
25
|
+
- [ ] End-to-end call testing pass/fail; turn-taking / barge-in verified.
|
|
26
|
+
- [ ] Transcript logging + PII redaction; recording-consent disclosure.
|
|
27
|
+
|
|
28
|
+
## Security & deploy
|
|
29
|
+
- [ ] `service_role` server-only; OpenRouter/Retell/ElevenLabs/Telnyx keys in env, never logged.
|
|
30
|
+
- [ ] `npx tsc --noEmit` exits 0; deploys to Vercel (+ Railway if worker).
|
|
31
|
+
- [ ] Post-deploy smoke includes **real agent calls**.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "NEXT_PUBLIC_SUPABASE_URL",
|
|
4
|
+
"purpose": "Supabase project URL — used by both client and server SDKs.",
|
|
5
|
+
"howToObtain": "Supabase Dashboard → Project Settings → API → Project URL, or `npx supabase projects list`.",
|
|
6
|
+
"ownerIssued": false
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"name": "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY",
|
|
10
|
+
"purpose": "Public (anon/publishable) key for client-side Supabase calls. Safe to expose; RLS protects data.",
|
|
11
|
+
"howToObtain": "Supabase Dashboard → Project Settings → API → Publishable/anon key.",
|
|
12
|
+
"ownerIssued": false
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "SUPABASE_SERVICE_ROLE_KEY",
|
|
16
|
+
"purpose": "Server-only service-role key for privileged mutations. NEVER prefixed NEXT_PUBLIC_, never imported into a client component.",
|
|
17
|
+
"howToObtain": "Supabase Dashboard → Project Settings → API → service_role key. Store server-side only.",
|
|
18
|
+
"ownerIssued": false
|
|
19
|
+
}
|
|
20
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Full App — Phase Plan
|
|
2
|
+
|
|
3
|
+
Stack: Next.js 16 (App Router) · Supabase (auth + Postgres + RLS) · Tailwind · Vercel.
|
|
4
|
+
Archetype: `web-app`. Seed plan copied into ROADMAP.md by `/qualia-new`.
|
|
5
|
+
|
|
6
|
+
## Phase 1: Foundation, Auth & Data
|
|
7
|
+
|
|
8
|
+
**Goal:** Stack deployed to a Vercel preview, Supabase wired with **RLS on every table from the first migration**, auth working end-to-end.
|
|
9
|
+
|
|
10
|
+
**Success criteria:**
|
|
11
|
+
1. User can sign up, log in, log out; session persists across refresh.
|
|
12
|
+
2. Every table has RLS enabled with policies derived from `app_metadata` claims.
|
|
13
|
+
3. RLS verified by logging in as two users — each sees only their own rows.
|
|
14
|
+
4. Role-based routing enforced in middleware AND at the data layer.
|
|
15
|
+
5. Deploys to a Vercel preview URL.
|
|
16
|
+
|
|
17
|
+
## Phase 2: Core Capabilities (vertical slices)
|
|
18
|
+
|
|
19
|
+
**Goal:** The primary user job works end-to-end — each capability cuts through UI + server action + RLS + validation + states.
|
|
20
|
+
|
|
21
|
+
**Success criteria:**
|
|
22
|
+
1. Primary user capability works end-to-end and persists.
|
|
23
|
+
2. Every async surface has loading / empty / error states.
|
|
24
|
+
3. Forms validate client AND server (Zod or equivalent); destructive actions confirm.
|
|
25
|
+
4. Mutations go through `lib/supabase/server.ts`, never the client.
|
|
26
|
+
|
|
27
|
+
## Phase 3: App Hardening
|
|
28
|
+
|
|
29
|
+
**Goal:** Production-grade — rate limiting, audit/soft-delete, performance, security headers.
|
|
30
|
+
|
|
31
|
+
**Success criteria:**
|
|
32
|
+
1. Rate limiting on mutating + public endpoints.
|
|
33
|
+
2. No N+1 on list views; perf budget met.
|
|
34
|
+
3. `service_role` confirmed server-only; security headers (HSTS) set.
|
|
35
|
+
4. Every UPDATE policy has a matching SELECT policy.
|
|
36
|
+
|
|
37
|
+
## Phase 4: Polish & Handoff
|
|
38
|
+
|
|
39
|
+
**Goal:** Design pass to anti-slop bar, a11y, prod deploy, credentials handover.
|
|
40
|
+
|
|
41
|
+
**Success criteria:**
|
|
42
|
+
1. DESIGN.md anti-slop bar met; a11y WCAG 2.2 AA.
|
|
43
|
+
2. Analytics + Sentry; custom domain; production deploy + smoke.
|
|
44
|
+
3. RLS / headers / env / MFA security pass.
|
|
45
|
+
4. Credentials, walkthrough, archive, `/qualia-report` to ERP.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Supabase — copy to .env.local and fill from your project.
|
|
2
|
+
# `npx supabase projects list` or Dashboard → Project Settings → API.
|
|
3
|
+
NEXT_PUBLIC_SUPABASE_URL=
|
|
4
|
+
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
|
|
5
|
+
|
|
6
|
+
# Server-only. NEVER prefix with NEXT_PUBLIC_. Never import into a client component.
|
|
7
|
+
SUPABASE_SERVICE_ROLE_KEY=
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Full App
|
|
2
|
+
|
|
3
|
+
Authenticated product — Next.js 16 + Supabase (auth + Postgres + RLS) + Tailwind + Vercel.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
cp .env.example .env.local # fill in Supabase keys
|
|
10
|
+
npx supabase db push # apply migrations (or link a project first)
|
|
11
|
+
npm run dev # http://localhost:3000
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Where things live
|
|
15
|
+
|
|
16
|
+
- `app/page.tsx` — entry; reads the session via the server adapter.
|
|
17
|
+
- `lib/supabase/server.ts` — server adapter. **All mutations go here.**
|
|
18
|
+
- `lib/supabase/client.ts` — browser adapter (publishable key only).
|
|
19
|
+
- `supabase/migrations/` — schema. **Every table has RLS from the first migration.**
|
|
20
|
+
- `.env.example` — required env. `SUPABASE_SERVICE_ROLE_KEY` is server-only.
|
|
21
|
+
|
|
22
|
+
## Non-negotiables (constitution)
|
|
23
|
+
|
|
24
|
+
- RLS on every table; authorize on `app_metadata`, never `user_metadata`.
|
|
25
|
+
- `service_role` key never `NEXT_PUBLIC_`, never in a client component.
|
|
26
|
+
- Schema changes are migrations only — never hand-applied to remote.
|
|
27
|
+
|
|
28
|
+
This is an MVP skeleton. Build the real auth + capabilities per `.planning/ROADMAP.md`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/* OKLCH design tokens. Source of truth is .planning/DESIGN.md.
|
|
4
|
+
NEVER raw #000 / #fff; tint neutrals toward the brand hue. */
|
|
5
|
+
:root {
|
|
6
|
+
--brand: oklch(0.62 0.18 264);
|
|
7
|
+
--ink: oklch(0.21 0.01 264);
|
|
8
|
+
--paper: oklch(0.99 0.004 264);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
background: var(--paper);
|
|
13
|
+
color: var(--ink);
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import "./globals.css";
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: "Full App",
|
|
6
|
+
description: "Replace with your product's one-sentence pitch.",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function RootLayout({
|
|
10
|
+
children,
|
|
11
|
+
}: {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<body>{children}</body>
|
|
17
|
+
</html>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createClient } from "@/lib/supabase/server";
|
|
2
|
+
|
|
3
|
+
export default async function Home() {
|
|
4
|
+
// Server Component reading the session through the RLS-aware adapter.
|
|
5
|
+
const supabase = await createClient();
|
|
6
|
+
const {
|
|
7
|
+
data: { user },
|
|
8
|
+
} = await supabase.auth.getUser();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<main className="mx-auto flex min-h-dvh max-w-3xl flex-col justify-center gap-6 px-6">
|
|
12
|
+
<h1 className="text-4xl font-semibold tracking-tight">Full App</h1>
|
|
13
|
+
<p className="text-lg text-neutral-600">
|
|
14
|
+
{user
|
|
15
|
+
? `Signed in as ${user.email}.`
|
|
16
|
+
: "Not signed in. Wire the auth flow in Phase 1, then build your core capability."}
|
|
17
|
+
</p>
|
|
18
|
+
</main>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createBrowserClient } from "@supabase/ssr";
|
|
2
|
+
|
|
3
|
+
// Client-side Supabase adapter. Uses the publishable (anon) key only.
|
|
4
|
+
// NEVER import the service-role key here — RLS protects the data.
|
|
5
|
+
export function createClient() {
|
|
6
|
+
return createBrowserClient(
|
|
7
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
8
|
+
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createServerClient } from "@supabase/ssr";
|
|
2
|
+
import { cookies } from "next/headers";
|
|
3
|
+
|
|
4
|
+
// Server-side Supabase adapter. Use this for all mutations and any
|
|
5
|
+
// RLS-protected read that needs the caller's session. The only seam that
|
|
6
|
+
// touches @supabase/ssr on the server.
|
|
7
|
+
export async function createClient() {
|
|
8
|
+
const cookieStore = await cookies();
|
|
9
|
+
|
|
10
|
+
return createServerClient(
|
|
11
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
12
|
+
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
|
|
13
|
+
{
|
|
14
|
+
cookies: {
|
|
15
|
+
getAll() {
|
|
16
|
+
return cookieStore.getAll();
|
|
17
|
+
},
|
|
18
|
+
setAll(cookiesToSet) {
|
|
19
|
+
try {
|
|
20
|
+
cookiesToSet.forEach(({ name, value, options }) =>
|
|
21
|
+
cookieStore.set(name, value, options)
|
|
22
|
+
);
|
|
23
|
+
} catch {
|
|
24
|
+
// Called from a Server Component — safe to ignore when middleware
|
|
25
|
+
// refreshes the session.
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "full-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "next lint",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"next": "^16.0.0",
|
|
14
|
+
"react": "^19.0.0",
|
|
15
|
+
"react-dom": "^19.0.0",
|
|
16
|
+
"@supabase/supabase-js": "^2.45.0",
|
|
17
|
+
"@supabase/ssr": "^0.5.0",
|
|
18
|
+
"zod": "^3.23.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.0.0",
|
|
22
|
+
"@types/react": "^19.0.0",
|
|
23
|
+
"@types/react-dom": "^19.0.0",
|
|
24
|
+
"tailwindcss": "^4.0.0",
|
|
25
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
26
|
+
"postcss": "^8.4.0",
|
|
27
|
+
"typescript": "^5.6.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
-- Initial migration. RLS is enabled FROM THE FIRST MIGRATION, not retrofitted.
|
|
2
|
+
-- Replace `profiles` with your real domain tables — keep the RLS pattern.
|
|
3
|
+
|
|
4
|
+
create table if not exists public.profiles (
|
|
5
|
+
id uuid primary key references auth.users (id) on delete cascade,
|
|
6
|
+
display_name text,
|
|
7
|
+
created_at timestamptz not null default now()
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
-- RLS on every table (constitution).
|
|
11
|
+
alter table public.profiles enable row level security;
|
|
12
|
+
|
|
13
|
+
-- A user may read their own row.
|
|
14
|
+
create policy "profiles_select_own"
|
|
15
|
+
on public.profiles for select
|
|
16
|
+
using (auth.uid() = id);
|
|
17
|
+
|
|
18
|
+
-- A user may update their own row. (Every UPDATE policy has a matching SELECT.)
|
|
19
|
+
create policy "profiles_update_own"
|
|
20
|
+
on public.profiles for update
|
|
21
|
+
using (auth.uid() = id)
|
|
22
|
+
with check (auth.uid() = id);
|
|
23
|
+
|
|
24
|
+
-- A user may insert their own row.
|
|
25
|
+
create policy "profiles_insert_own"
|
|
26
|
+
on public.profiles for insert
|
|
27
|
+
with check (auth.uid() = id);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "ES2022"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noEmit": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"jsx": "preserve",
|
|
15
|
+
"incremental": true,
|
|
16
|
+
"plugins": [{ "name": "next" }],
|
|
17
|
+
"paths": { "@/*": ["./*"] }
|
|
18
|
+
},
|
|
19
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
20
|
+
"exclude": ["node_modules"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "full-app",
|
|
3
|
+
"title": "Full App",
|
|
4
|
+
"description": "Authenticated product — Supabase auth + Postgres + RLS, Tailwind UI, deployed on Vercel. The default for anything with user accounts and persisted data.",
|
|
5
|
+
"archetype": "web-app",
|
|
6
|
+
"defaultStack": "Next.js 16 (App Router) · Supabase (auth + Postgres + RLS) · Tailwind · Vercel",
|
|
7
|
+
"scaffoldEntry": "app/page.tsx",
|
|
8
|
+
"whenToUse": "Any product with logged-in users, roles, and a database. Pick this when users sign up, data persists per-user, and RLS must isolate tenants."
|
|
9
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Full App — Definition of Done
|
|
2
|
+
|
|
3
|
+
Per-stack verify checklist. Inherits `rules/constitution.md` and the `web-app` archetype DoD.
|
|
4
|
+
|
|
5
|
+
## Auth & Access (constitution — non-negotiable)
|
|
6
|
+
- [ ] Supabase auth with the chosen model; email verification + password reset wired.
|
|
7
|
+
- [ ] **RLS enabled on every table**, policies derived from `app_metadata` (never `user_metadata`).
|
|
8
|
+
- [ ] RLS verified as two+ users — each sees only its own rows.
|
|
9
|
+
- [ ] Every UPDATE policy has a matching SELECT policy.
|
|
10
|
+
- [ ] Postgres views set `security_invoker = true`.
|
|
11
|
+
- [ ] Storage upsert grants INSERT + SELECT + UPDATE on the bucket.
|
|
12
|
+
- [ ] Sessions revoked before a user is deleted.
|
|
13
|
+
|
|
14
|
+
## Security
|
|
15
|
+
- [ ] `SUPABASE_SERVICE_ROLE_KEY` is server-only — never `NEXT_PUBLIC_`, never imported in a client component.
|
|
16
|
+
- [ ] Mutations use `lib/supabase/server.ts`; client reads use `lib/supabase/client.ts`.
|
|
17
|
+
- [ ] Inputs validated with Zod; no `dangerouslySetInnerHTML`, no `eval()`.
|
|
18
|
+
- [ ] Security headers (HSTS); CSRF/permission checks on every mutation.
|
|
19
|
+
|
|
20
|
+
## Schema flow
|
|
21
|
+
- [ ] All schema changes are migrations in `supabase/migrations/` — never hand-applied to remote.
|
|
22
|
+
- [ ] `npx supabase gen types` run after schema changes.
|
|
23
|
+
|
|
24
|
+
## App quality
|
|
25
|
+
- [ ] Every async surface has loading / empty / error states.
|
|
26
|
+
- [ ] Destructive actions confirm; rate limiting on mutating + public endpoints.
|
|
27
|
+
- [ ] No N+1 on list views.
|
|
28
|
+
|
|
29
|
+
## Build & Deploy
|
|
30
|
+
- [ ] `npx tsc --noEmit` exits 0; `next build` succeeds.
|
|
31
|
+
- [ ] Deploys to Vercel; homepage returns HTTP 200; auth callback responds.
|
|
32
|
+
- [ ] Design anti-slop bar met (DESIGN.md); a11y AA.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "NEXT_PUBLIC_SUPABASE_URL",
|
|
4
|
+
"purpose": "Supabase project URL — used by both client and server SDKs.",
|
|
5
|
+
"howToObtain": "Supabase Dashboard → Project Settings → API → Project URL, or `npx supabase projects list`.",
|
|
6
|
+
"ownerIssued": false
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"name": "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY",
|
|
10
|
+
"purpose": "Public (anon/publishable) key for client-side Supabase calls. RLS protects data.",
|
|
11
|
+
"howToObtain": "Supabase Dashboard → Project Settings → API → Publishable/anon key.",
|
|
12
|
+
"ownerIssued": false
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "SUPABASE_SERVICE_ROLE_KEY",
|
|
16
|
+
"purpose": "Server-only service-role key — also used to invite staff users from a server action. NEVER NEXT_PUBLIC_, never in a client component.",
|
|
17
|
+
"howToObtain": "Supabase Dashboard → Project Settings → API → service_role key.",
|
|
18
|
+
"ownerIssued": false
|
|
19
|
+
}
|
|
20
|
+
]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Internal Tool — Phase Plan
|
|
2
|
+
|
|
3
|
+
Stack: Next.js 16 (App Router) · Supabase (portal auth + RLS) · Tailwind · Vercel.
|
|
4
|
+
Archetype: `web-app` (internal/living product — runs as a rolling release, no terminal Handoff).
|
|
5
|
+
Seed plan copied into ROADMAP.md by `/qualia-new`.
|
|
6
|
+
|
|
7
|
+
## Phase 1: Foundation, Portal Auth & Roles
|
|
8
|
+
|
|
9
|
+
**Goal:** Stack on a Vercel preview; Supabase wired with RLS on every table; invite-only auth (no public signup); roles in `app_metadata`.
|
|
10
|
+
|
|
11
|
+
**Success criteria:**
|
|
12
|
+
1. Staff log in (invite / SSO); public signup is disabled.
|
|
13
|
+
2. Roles stored in `app_metadata`; role-based routing in middleware AND data layer.
|
|
14
|
+
3. Every table has RLS; verified as two roles — each sees only its scope.
|
|
15
|
+
4. Deploys to a Vercel preview URL.
|
|
16
|
+
|
|
17
|
+
## Phase 2: Core Workflows (vertical slices)
|
|
18
|
+
|
|
19
|
+
**Goal:** The primary staff workflows work end-to-end — list, detail, create/edit, with states.
|
|
20
|
+
|
|
21
|
+
**Success criteria:**
|
|
22
|
+
1. Primary workflow works end-to-end and persists.
|
|
23
|
+
2. Every async surface has loading / empty / error states.
|
|
24
|
+
3. Forms validate client AND server; destructive actions confirm + soft-delete where flagged.
|
|
25
|
+
4. Mutations go through `lib/supabase/server.ts`.
|
|
26
|
+
|
|
27
|
+
## Phase 3: Admin, Audit & Hardening
|
|
28
|
+
|
|
29
|
+
**Goal:** User management, audit trail, rate limiting, performance.
|
|
30
|
+
|
|
31
|
+
**Success criteria:**
|
|
32
|
+
1. Admin can invite / deactivate users and assign roles.
|
|
33
|
+
2. Audit log on sensitive writes; soft-delete where flagged.
|
|
34
|
+
3. No N+1 on list views; rate limiting on mutating endpoints.
|
|
35
|
+
4. Every UPDATE policy has a matching SELECT policy.
|
|
36
|
+
|
|
37
|
+
## Phase 4: Polish & Rolling Release
|
|
38
|
+
|
|
39
|
+
**Goal:** Design pass, a11y, prod deploy. As a living product this repeats per increment — no terminal Handoff.
|
|
40
|
+
|
|
41
|
+
**Success criteria:**
|
|
42
|
+
1. DESIGN.md anti-slop bar; a11y WCAG 2.2 AA.
|
|
43
|
+
2. `service_role` server-only; security headers; MFA on accounts.
|
|
44
|
+
3. Production deploy + smoke (HTTP 200, auth callback, role isolation).
|
|
45
|
+
4. `/qualia-report` per increment.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Supabase — internal portal. Public signup disabled; staff join by invite.
|
|
2
|
+
NEXT_PUBLIC_SUPABASE_URL=
|
|
3
|
+
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=
|
|
4
|
+
|
|
5
|
+
# Server-only — also used to invite staff from a server action.
|
|
6
|
+
# NEVER prefix with NEXT_PUBLIC_, never import into a client component.
|
|
7
|
+
SUPABASE_SERVICE_ROLE_KEY=
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Internal Tool
|
|
2
|
+
|
|
3
|
+
Staff-facing portal — Next.js 16 + Supabase (invite-only auth + RLS) + Tailwind + Vercel.
|
|
4
|
+
Public signup is disabled; staff join by invite / SSO. Runs as a rolling release.
|
|
5
|
+
|
|
6
|
+
## Run
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
cp .env.example .env.local # fill Supabase keys
|
|
11
|
+
npx supabase db push
|
|
12
|
+
npm run dev # http://localhost:3000
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Where things live
|
|
16
|
+
|
|
17
|
+
- `app/page.tsx` — portal entry; reads session + role from `app_metadata`.
|
|
18
|
+
- `lib/supabase/server.ts` — server adapter (mutations, RLS, inviting staff).
|
|
19
|
+
- `lib/supabase/client.ts` — browser adapter (publishable key only).
|
|
20
|
+
- `supabase/migrations/` — domain tables, RLS + role gating from the first migration.
|
|
21
|
+
|
|
22
|
+
## Non-negotiables (constitution)
|
|
23
|
+
|
|
24
|
+
- Roles live in `app_metadata`, never `user_metadata`.
|
|
25
|
+
- RLS on every table; every UPDATE policy has a matching SELECT.
|
|
26
|
+
- `service_role` server-only; used for inviting users from a server action.
|
|
27
|
+
- Disable public signup in the Supabase Auth settings.
|
|
28
|
+
|
|
29
|
+
This is an MVP skeleton. Build portal auth + workflows per `.planning/ROADMAP.md`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/* OKLCH design tokens. Source of truth is .planning/DESIGN.md. */
|
|
4
|
+
:root {
|
|
5
|
+
--brand: oklch(0.62 0.18 264);
|
|
6
|
+
--ink: oklch(0.21 0.01 264);
|
|
7
|
+
--paper: oklch(0.99 0.004 264);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
background: var(--paper);
|
|
12
|
+
color: var(--ink);
|
|
13
|
+
}
|