diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..18a0ff5
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,5 @@
+# Copy this to .env.local for local dev
+# In Dokploy, set these as environment variables directly
+
+NEXT_PUBLIC_SUPABASE_URL=https://your-supabase.yourdomain.com
+NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d325cf2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,28 @@
+# deps
+node_modules/
+.pnp
+.pnp.js
+
+# build
+.next/
+out/
+dist/
+
+# env
+.env
+.env.local
+.env.*.local
+
+# misc
+.DS_Store
+*.pem
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..585488b
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,38 @@
+# ── Stage 1: deps ──────────────────────────────────────────────────────────
+FROM node:20-alpine AS deps
+WORKDIR /app
+COPY package.json package-lock.json* ./
+RUN npm ci
+
+# ── Stage 2: build ─────────────────────────────────────────────────────────
+FROM node:20-alpine AS builder
+WORKDIR /app
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+
+# Build args become env vars at build time for the Next.js public bundle
+ARG NEXT_PUBLIC_SUPABASE_URL
+ARG NEXT_PUBLIC_SUPABASE_ANON_KEY
+ENV NEXT_PUBLIC_SUPABASE_URL=$NEXT_PUBLIC_SUPABASE_URL
+ENV NEXT_PUBLIC_SUPABASE_ANON_KEY=$NEXT_PUBLIC_SUPABASE_ANON_KEY
+
+RUN npm run build
+
+# ── Stage 3: runner ────────────────────────────────────────────────────────
+FROM node:20-alpine AS runner
+WORKDIR /app
+ENV NODE_ENV=production
+
+RUN addgroup --system --gid 1001 nodejs \
+ && adduser --system --uid 1001 nextjs
+
+COPY --from=builder /app/public ./public
+COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
+
+USER nextjs
+EXPOSE 3000
+ENV PORT=3000
+ENV HOSTNAME="0.0.0.0"
+
+CMD ["node", "server.js"]
diff --git a/app/api/health/route.ts b/app/api/health/route.ts
new file mode 100644
index 0000000..2109c80
--- /dev/null
+++ b/app/api/health/route.ts
@@ -0,0 +1,14 @@
+import { NextResponse } from "next/server";
+
+export const runtime = "edge";
+
+export function GET() {
+ return NextResponse.json(
+ {
+ status: "ok",
+ app: "fireside-test-app",
+ ts: new Date().toISOString(),
+ },
+ { status: 200 }
+ );
+}
diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx
new file mode 100644
index 0000000..179a0cb
--- /dev/null
+++ b/app/auth/login/page.tsx
@@ -0,0 +1,17 @@
+import Link from "next/link";
+import { AuthForm } from "@/components/AuthForm";
+
+export default function LoginPage() {
+ return (
+
+
+
Sign in
+
// fireside-test-app · auth smoke test
+
+
+ No account? Sign up →
+
+
+
+ );
+}
diff --git a/app/auth/signup/page.tsx b/app/auth/signup/page.tsx
new file mode 100644
index 0000000..271698e
--- /dev/null
+++ b/app/auth/signup/page.tsx
@@ -0,0 +1,17 @@
+import Link from "next/link";
+import { AuthForm } from "@/components/AuthForm";
+
+export default function SignupPage() {
+ return (
+
+
+
Create account
+
// fireside-test-app · new user registration
+
+
+ Already have an account? Sign in →
+
+
+
+ );
+}
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
new file mode 100644
index 0000000..d5811d3
--- /dev/null
+++ b/app/dashboard/page.tsx
@@ -0,0 +1,49 @@
+import { redirect } from "next/navigation";
+import { createClient } from "@/lib/supabase/server";
+import { NotesList } from "@/components/NotesList";
+import { SignOutButton } from "@/components/SignOutButton";
+
+export default async function DashboardPage() {
+ const supabase = await createClient();
+
+ const { data: { user }, error: authError } = await supabase.auth.getUser();
+ if (authError || !user) redirect("/auth/login");
+
+ const { data: notes, error: notesError } = await supabase
+ .from("notes")
+ .select("id, body, created_at")
+ .eq("user_id", user.id)
+ .order("created_at", { ascending: false });
+
+ return (
+
+
+
+ FIRESIDE TEST
+
+
+ {user.email}
+
+
+
+
+
+
+ Forgejo
+ Dokploy
+ Supabase Auth ✓
+ Supabase DB ✓
+ Next.js 15 SSR
+
+
+ // personal notes — user_id: {user.id.slice(0, 8)}…
+
+ {notesError && (
+ DB error: {notesError.message}
+ )}
+
+
+
+
+ );
+}
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..32d2931
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,306 @@
+@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Syne:wght@400;600;700;800&display=swap');
+
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+
+:root {
+ --bg: #0d0d0d;
+ --surface: #161616;
+ --border: #2a2a2a;
+ --amber: #f59e0b;
+ --amber-dim: #92400e;
+ --text: #e8e8e8;
+ --muted: #666;
+ --danger: #ef4444;
+ --success: #22c55e;
+ --radius: 4px;
+ --mono: 'IBM Plex Mono', monospace;
+ --sans: 'Syne', sans-serif;
+}
+
+html, body { height: 100%; background: var(--bg); color: var(--text); font-family: var(--sans); }
+
+/* ── Utilities ────────────────────────────────── */
+.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
+
+/* ── Page shell ───────────────────────────────── */
+.page {
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem;
+ background:
+ radial-gradient(ellipse 60% 40% at 50% 0%, #1c1200 0%, transparent 70%),
+ var(--bg);
+}
+
+/* ── Card ─────────────────────────────────────── */
+.card {
+ width: 100%;
+ max-width: 440px;
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 2.5rem;
+}
+
+.card-title {
+ font-size: 1.6rem;
+ font-weight: 800;
+ letter-spacing: -0.03em;
+ margin-bottom: 0.25rem;
+}
+
+.card-sub {
+ font-family: var(--mono);
+ font-size: 0.75rem;
+ color: var(--muted);
+ margin-bottom: 2rem;
+}
+
+/* ── Form ─────────────────────────────────────── */
+.field { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 1.25rem; }
+
+label {
+ font-family: var(--mono);
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: var(--muted);
+}
+
+input[type="email"],
+input[type="password"],
+input[type="text"],
+textarea {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ color: var(--text);
+ font-family: var(--mono);
+ font-size: 0.875rem;
+ padding: 0.65rem 0.9rem;
+ transition: border-color 0.15s;
+ width: 100%;
+ resize: vertical;
+}
+
+input:focus, textarea:focus {
+ outline: none;
+ border-color: var(--amber);
+}
+
+/* ── Buttons ──────────────────────────────────── */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ font-family: var(--mono);
+ font-size: 0.8rem;
+ font-weight: 500;
+ letter-spacing: 0.05em;
+ padding: 0.7rem 1.25rem;
+ border-radius: var(--radius);
+ cursor: pointer;
+ border: none;
+ transition: opacity 0.15s, transform 0.1s;
+}
+.btn:active { transform: scale(0.98); }
+.btn:disabled { opacity: 0.4; cursor: not-allowed; }
+
+.btn-primary {
+ background: var(--amber);
+ color: #000;
+ width: 100%;
+}
+.btn-primary:hover:not(:disabled) { opacity: 0.88; }
+
+.btn-ghost {
+ background: transparent;
+ color: var(--muted);
+ border: 1px solid var(--border);
+}
+.btn-ghost:hover { color: var(--text); border-color: var(--text); }
+
+.btn-danger {
+ background: transparent;
+ color: var(--danger);
+ border: 1px solid #3a1a1a;
+ font-size: 0.75rem;
+ padding: 0.35rem 0.75rem;
+}
+.btn-danger:hover { background: #1a0000; border-color: var(--danger); }
+
+/* ── Feedback ─────────────────────────────────── */
+.error-msg {
+ font-family: var(--mono);
+ font-size: 0.75rem;
+ color: var(--danger);
+ background: #1a0000;
+ border: 1px solid #3a0000;
+ border-radius: var(--radius);
+ padding: 0.6rem 0.9rem;
+ margin-bottom: 1rem;
+}
+
+.success-msg {
+ font-family: var(--mono);
+ font-size: 0.75rem;
+ color: var(--success);
+ background: #001a00;
+ border: 1px solid #003a00;
+ border-radius: var(--radius);
+ padding: 0.6rem 0.9rem;
+ margin-bottom: 1rem;
+}
+
+/* ── Link ─────────────────────────────────────── */
+.link {
+ font-family: var(--mono);
+ font-size: 0.75rem;
+ color: var(--amber);
+ text-decoration: none;
+ text-align: center;
+ display: block;
+ margin-top: 1.25rem;
+}
+.link:hover { text-decoration: underline; }
+
+/* ── Dashboard layout ─────────────────────────── */
+.dashboard {
+ min-height: 100vh;
+ display: grid;
+ grid-template-rows: auto 1fr;
+ background: var(--bg);
+}
+
+.topbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem 2rem;
+ border-bottom: 1px solid var(--border);
+ background: var(--surface);
+}
+
+.topbar-brand {
+ font-size: 0.9rem;
+ font-weight: 700;
+ letter-spacing: 0.05em;
+}
+
+.topbar-brand span {
+ color: var(--amber);
+}
+
+.topbar-user {
+ font-family: var(--mono);
+ font-size: 0.72rem;
+ color: var(--muted);
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.main-content {
+ max-width: 760px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 2.5rem 2rem;
+}
+
+/* ── Notes ────────────────────────────────────── */
+.section-label {
+ font-family: var(--mono);
+ font-size: 0.68rem;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ color: var(--amber-dim);
+ margin-bottom: 1rem;
+}
+
+.note-form {
+ display: flex;
+ gap: 0.75rem;
+ margin-bottom: 2rem;
+ align-items: flex-end;
+}
+
+.note-form textarea {
+ flex: 1;
+ min-height: 72px;
+}
+
+.note-form .btn-primary {
+ width: auto;
+ white-space: nowrap;
+ align-self: flex-end;
+}
+
+.notes-list { display: flex; flex-direction: column; gap: 0.75rem; }
+
+.note-item {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 1rem 1.1rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 1rem;
+ animation: fadeIn 0.2s ease;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; transform: translateY(6px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+.note-body {
+ font-size: 0.875rem;
+ line-height: 1.6;
+ flex: 1;
+ word-break: break-word;
+ white-space: pre-wrap;
+}
+
+.note-time {
+ font-family: var(--mono);
+ font-size: 0.65rem;
+ color: var(--muted);
+ margin-top: 0.4rem;
+}
+
+.empty-state {
+ font-family: var(--mono);
+ font-size: 0.78rem;
+ color: var(--muted);
+ text-align: center;
+ padding: 3rem 0;
+ border: 1px dashed var(--border);
+ border-radius: var(--radius);
+}
+
+/* ── Stack badge ──────────────────────────────── */
+.stack-badge {
+ display: flex;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+ margin-bottom: 2rem;
+}
+
+.badge {
+ font-family: var(--mono);
+ font-size: 0.65rem;
+ padding: 0.25rem 0.6rem;
+ border-radius: 2px;
+ background: #1a1a1a;
+ border: 1px solid var(--border);
+ color: var(--muted);
+}
+
+.badge.active {
+ border-color: var(--amber-dim);
+ color: var(--amber);
+}
diff --git a/app/layout.tsx b/app/layout.tsx
new file mode 100644
index 0000000..bc6c701
--- /dev/null
+++ b/app/layout.tsx
@@ -0,0 +1,19 @@
+import type { Metadata } from "next";
+import "./globals.css";
+
+export const metadata: Metadata = {
+ title: "Fireside Test App",
+ description: "End-to-end deployment test — Forgejo · Dokploy · Supabase",
+};
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/app/page.tsx b/app/page.tsx
new file mode 100644
index 0000000..cf3f11c
--- /dev/null
+++ b/app/page.tsx
@@ -0,0 +1,10 @@
+import { redirect } from "next/navigation";
+import { createClient } from "@/lib/supabase/server";
+
+export default async function Home() {
+ const supabase = await createClient();
+ const { data: { user } } = await supabase.auth.getUser();
+
+ if (user) redirect("/dashboard");
+ else redirect("/auth/login");
+}
diff --git a/components/AuthForm.tsx b/components/AuthForm.tsx
new file mode 100644
index 0000000..ec72703
--- /dev/null
+++ b/components/AuthForm.tsx
@@ -0,0 +1,75 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { createClient } from "@/lib/supabase/client";
+
+export function AuthForm({ mode }: { mode: "login" | "signup" }) {
+ const router = useRouter();
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState("");
+ const [info, setInfo] = useState("");
+ const [loading, setLoading] = useState(false);
+
+ async function handleSubmit() {
+ setError("");
+ setInfo("");
+ setLoading(true);
+
+ const supabase = createClient();
+
+ if (mode === "signup") {
+ const { error } = await supabase.auth.signUp({ email, password });
+ if (error) { setError(error.message); setLoading(false); return; }
+ setInfo("Account created — check your email to confirm, then sign in.");
+ setLoading(false);
+ return;
+ }
+
+ const { error } = await supabase.auth.signInWithPassword({ email, password });
+ if (error) { setError(error.message); setLoading(false); return; }
+ router.push("/dashboard");
+ router.refresh();
+ }
+
+ return (
+ <>
+ {error && {error}
}
+ {info && {info}
}
+
+
+
+ setEmail(e.target.value)}
+ placeholder="you@example.com"
+ autoComplete="email"
+ />
+
+
+
+
+ setPassword(e.target.value)}
+ placeholder="••••••••"
+ autoComplete={mode === "login" ? "current-password" : "new-password"}
+ onKeyDown={e => e.key === "Enter" && handleSubmit()}
+ />
+
+
+
+ >
+ );
+}
diff --git a/components/NotesList.tsx b/components/NotesList.tsx
new file mode 100644
index 0000000..2dd22c0
--- /dev/null
+++ b/components/NotesList.tsx
@@ -0,0 +1,96 @@
+"use client";
+
+import { useState } from "react";
+import { createClient } from "@/lib/supabase/client";
+
+type Note = { id: string; body: string; created_at: string };
+
+export function NotesList({
+ initialNotes,
+ userId,
+}: {
+ initialNotes: Note[];
+ userId: string;
+}) {
+ const [notes, setNotes] = useState(initialNotes);
+ const [body, setBody] = useState("");
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+
+ const supabase = createClient();
+
+ async function addNote() {
+ if (!body.trim()) return;
+ setLoading(true);
+ setError("");
+
+ const { data, error } = await supabase
+ .from("notes")
+ .insert({ body: body.trim(), user_id: userId })
+ .select("id, body, created_at")
+ .single();
+
+ if (error) { setError(error.message); setLoading(false); return; }
+ setNotes([data, ...notes]);
+ setBody("");
+ setLoading(false);
+ }
+
+ async function deleteNote(id: string) {
+ const { error } = await supabase.from("notes").delete().eq("id", id);
+ if (error) { setError(error.message); return; }
+ setNotes(notes.filter(n => n.id !== id));
+ }
+
+ function fmt(ts: string) {
+ return new Date(ts).toLocaleString("en-KE", {
+ dateStyle: "medium",
+ timeStyle: "short",
+ });
+ }
+
+ return (
+ <>
+ {error && {error}
}
+
+
+
+
+
+ {notes.length === 0 ? (
+
+ No notes yet. Add one above to verify the DB write path.
+
+ ) : (
+ notes.map(note => (
+
+
+
{note.body}
+
{fmt(note.created_at)}
+
+
+
+ ))
+ )}
+
+ >
+ );
+}
diff --git a/components/SignOutButton.tsx b/components/SignOutButton.tsx
new file mode 100644
index 0000000..0c26edf
--- /dev/null
+++ b/components/SignOutButton.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { useRouter } from "next/navigation";
+import { createClient } from "@/lib/supabase/client";
+
+export function SignOutButton() {
+ const router = useRouter();
+
+ async function signOut() {
+ const supabase = createClient();
+ await supabase.auth.signOut();
+ router.push("/auth/login");
+ router.refresh();
+ }
+
+ return (
+
+ );
+}
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..fe9adce
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,24 @@
+version: "3.9"
+
+services:
+ app:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ args:
+ NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
+ image: fireside-test-app:latest
+ restart: unless-stopped
+ ports:
+ - "3000:3000"
+ environment:
+ NODE_ENV: production
+ NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL}
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY}
+ healthcheck:
+ test: ["CMD", "wget", "-qO-", "http://localhost:3000/api/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 20s
diff --git a/lib/supabase/client.ts b/lib/supabase/client.ts
new file mode 100644
index 0000000..9811d2f
--- /dev/null
+++ b/lib/supabase/client.ts
@@ -0,0 +1,10 @@
+"use client";
+
+import { createBrowserClient } from "@supabase/ssr";
+
+export function createClient() {
+ return createBrowserClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
+ );
+}
diff --git a/lib/supabase/server.ts b/lib/supabase/server.ts
new file mode 100644
index 0000000..19bd99c
--- /dev/null
+++ b/lib/supabase/server.ts
@@ -0,0 +1,27 @@
+import { createServerClient } from "@supabase/ssr";
+import { cookies } from "next/headers";
+
+export async function createClient() {
+ const cookieStore = await cookies();
+
+ return createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return cookieStore.getAll();
+ },
+ setAll(cookiesToSet) {
+ try {
+ cookiesToSet.forEach(({ name, value, options }) =>
+ cookieStore.set(name, value, options)
+ );
+ } catch {
+ // Server Component — middleware handles session refresh
+ }
+ },
+ },
+ }
+ );
+}
diff --git a/middleware.ts b/middleware.ts
new file mode 100644
index 0000000..1a0a47f
--- /dev/null
+++ b/middleware.ts
@@ -0,0 +1,55 @@
+import { createServerClient } from "@supabase/ssr";
+import { NextResponse, type NextRequest } from "next/server";
+
+export async function middleware(request: NextRequest) {
+ let supabaseResponse = NextResponse.next({ request });
+
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ getAll() {
+ return request.cookies.getAll();
+ },
+ setAll(cookiesToSet) {
+ cookiesToSet.forEach(({ name, value }) =>
+ request.cookies.set(name, value)
+ );
+ supabaseResponse = NextResponse.next({ request });
+ cookiesToSet.forEach(({ name, value, options }) =>
+ supabaseResponse.cookies.set(name, value, options)
+ );
+ },
+ },
+ }
+ );
+
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+
+ // Protect /dashboard routes
+ if (!user && request.nextUrl.pathname.startsWith("/dashboard")) {
+ const url = request.nextUrl.clone();
+ url.pathname = "/auth/login";
+ return NextResponse.redirect(url);
+ }
+
+ // Redirect logged-in users away from auth pages
+ if (
+ user &&
+ (request.nextUrl.pathname === "/auth/login" ||
+ request.nextUrl.pathname === "/auth/signup")
+ ) {
+ const url = request.nextUrl.clone();
+ url.pathname = "/dashboard";
+ return NextResponse.redirect(url);
+ }
+
+ return supabaseResponse;
+}
+
+export const config = {
+ matcher: ["/dashboard/:path*", "/auth/:path*"],
+};
diff --git a/next.config.ts b/next.config.ts
new file mode 100644
index 0000000..68a6c64
--- /dev/null
+++ b/next.config.ts
@@ -0,0 +1,7 @@
+import type { NextConfig } from "next";
+
+const nextConfig: NextConfig = {
+ output: "standalone",
+};
+
+export default nextConfig;
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f9e98a9
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "fireside-test-app",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@supabase/ssr": "^0.5.2",
+ "@supabase/supabase-js": "^2.49.4",
+ "next": "15.3.1",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20",
+ "@types/react": "^19",
+ "@types/react-dom": "^19",
+ "typescript": "^5"
+ }
+}
diff --git a/supabase/migrations/001_notes.sql b/supabase/migrations/001_notes.sql
new file mode 100644
index 0000000..9611ae9
--- /dev/null
+++ b/supabase/migrations/001_notes.sql
@@ -0,0 +1,23 @@
+-- Run this in your Supabase SQL editor or via psql
+
+create table if not exists public.notes (
+ id uuid primary key default gen_random_uuid(),
+ user_id uuid not null references auth.users(id) on delete cascade,
+ body text not null check (char_length(body) > 0),
+ created_at timestamptz not null default now()
+);
+
+-- Row Level Security: users can only touch their own notes
+alter table public.notes enable row level security;
+
+create policy "Users can select own notes"
+ on public.notes for select
+ using (auth.uid() = user_id);
+
+create policy "Users can insert own notes"
+ on public.notes for insert
+ with check (auth.uid() = user_id);
+
+create policy "Users can delete own notes"
+ on public.notes for delete
+ using (auth.uid() = user_id);
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..455bf35
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [{ "name": "next" }],
+ "paths": { "@/*": ["./*"] }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}