# Phase 0 — Foundation Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Stand up a dockerized, dedicated backend service (Hono + better-auth + Drizzle + SQLite/libsql) where a client can sign up, sign in, and use the returned token to call a protected endpoint — proven end-to-end by automated tests and `docker compose up`. **Architecture:** A new greenfield monorepo structure is added alongside the inherited code (which stays untouched as reference). A single backend service (`apps/api`) owns auth and all DB access; a shared package (`packages/shared`) holds the TypeScript/zod API contracts. The database is a local SQLite file accessed through libsql (no native compilation — Windows-friendly and identical in Docker). **Tech Stack:** Node 22, TypeScript (ESM), Hono, better-auth, Drizzle ORM + drizzle-kit, `@libsql/client`, zod, Vitest, tsx, Docker. ## Global Constraints - **Package manager:** Yarn 4 (Berry), already configured at the repo root. Run via corepack. - **Do NOT touch** the inherited `apps/mobile` or `apps/web` in this phase — they are reference only. - **Package names:** scoped `@solelog/*` (project is "SoleLog"). This phase adds `@solelog/api` and `@solelog/shared`. - **Module system:** ESM everywhere (`"type": "module"`), `moduleResolution: "Bundler"` or `"NodeNext"` in tsconfig. - **DB driver:** libsql (`@libsql/client`) only — never `better-sqlite3` (avoids native build on Windows). - **Dependency majors:** `hono@^4`, `@hono/node-server@^1`, `better-auth@^1`, `drizzle-orm@^0.36`, `drizzle-kit@^0.30`, `@libsql/client@^0.14`, `zod@^3`, `vitest@^3`, `tsx@^4`. Install the latest within each major. - **API port:** `3000` (the inherited web app used 4000 — avoid the clash). - **Env vars (apps/api):** `DATABASE_URL` (e.g. `file:./data/app.db`), `BETTER_AUTH_SECRET`, `BETTER_AUTH_URL` (e.g. `http://localhost:3000`), `PORT`. - **Commit style:** Conventional Commits (`feat:`, `chore:`, `test:`, `docs:`), one commit per task step where indicated. --- ## File Structure ``` apps/api/ package.json tsconfig.json drizzle.config.ts .env.example src/ index.ts entry — starts the HTTP server app.ts builds the Hono app (exported for tests) env.ts reads + validates env vars db/ client.ts libsql + drizzle client (singleton) schema.ts better-auth tables (Drizzle) migrate.ts applies drizzle migrations auth.ts better-auth config routes/ health.ts GET /health (public) me.ts GET /api/me (protected) drizzle/ generated migration SQL (created by drizzle-kit) test/ setup.ts vitest global setup: temp DB + migrate health.test.ts auth.test.ts vitest.config.ts Dockerfile packages/shared/ package.json tsconfig.json src/index.ts zod schemas + types (API contracts) docker-compose.yml (repo root) ``` --- ## Task 1: Monorepo wiring + shared contracts package **Files:** - Modify: `package.json` (root) — add `packages/*` to `workspaces` - Create: `packages/shared/package.json` - Create: `packages/shared/tsconfig.json` - Create: `packages/shared/src/index.ts` **Interfaces:** - Produces: `@solelog/shared` exporting `HealthResponse` (zod schema + type) and `MeResponse` (zod schema + type), consumed by `apps/api`. - [ ] **Step 1: Add `packages/*` to root workspaces** Edit root `package.json` so the `workspaces` array is: ```json "workspaces": [ "apps/*", "packages/*" ] ``` - [ ] **Step 2: Create the shared package manifest** `packages/shared/package.json`: ```json { "name": "@solelog/shared", "version": "0.0.0", "private": true, "type": "module", "main": "./src/index.ts", "types": "./src/index.ts", "exports": { ".": "./src/index.ts" }, "dependencies": { "zod": "^3.23.8" } } ``` - [ ] **Step 3: Create the shared tsconfig** `packages/shared/tsconfig.json`: ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "declaration": true, "skipLibCheck": true, "esModuleInterop": true, "verbatimModuleSyntax": true }, "include": ["src"] } ``` - [ ] **Step 4: Write the shared contracts** `packages/shared/src/index.ts`: ```ts import { z } from 'zod'; export const HealthResponse = z.object({ status: z.literal('ok'), }); export type HealthResponse = z.infer; export const PublicUser = z.object({ id: z.string(), email: z.string().email(), name: z.string(), }); export type PublicUser = z.infer; export const MeResponse = z.object({ user: PublicUser, }); export type MeResponse = z.infer; ``` - [ ] **Step 5: Install and verify the workspace resolves** Run: `yarn install` Expected: completes; `yarn workspaces list` includes `@solelog/shared`. Run: `yarn workspaces list` Expected output includes a line for `packages/shared`. - [ ] **Step 6: Commit** ```bash git add package.json yarn.lock packages/shared git commit -m "feat(shared): add @solelog/shared contracts package and wire packages/* workspace" ``` --- ## Task 2: Hono backend skeleton + health endpoint **Files:** - Create: `apps/api/package.json` - Create: `apps/api/tsconfig.json` - Create: `apps/api/src/env.ts` - Create: `apps/api/src/app.ts` - Create: `apps/api/src/routes/health.ts` - Create: `apps/api/src/index.ts` - Create: `apps/api/vitest.config.ts` - Test: `apps/api/test/health.test.ts` **Interfaces:** - Consumes: `@solelog/shared` (`HealthResponse`). - Produces: `createApp(): Hono` from `apps/api/src/app.ts` (used by every later task and all tests); `env` object from `apps/api/src/env.ts`. - [ ] **Step 1: Create the api package manifest** `apps/api/package.json`: ```json { "name": "@solelog/api", "version": "0.0.0", "private": true, "type": "module", "scripts": { "dev": "tsx watch src/index.ts", "start": "tsx src/index.ts", "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit", "db:generate": "drizzle-kit generate", "db:migrate": "tsx src/db/migrate.ts" }, "dependencies": { "@hono/node-server": "^1.13.7", "@libsql/client": "^0.14.0", "@solelog/shared": "workspace:*", "better-auth": "^1.1.7", "drizzle-orm": "^0.36.4", "hono": "^4.6.14", "zod": "^3.23.8" }, "devDependencies": { "drizzle-kit": "^0.30.1", "tsx": "^4.19.2", "typescript": "^5.7.2", "vitest": "^3.0.0" } } ``` - [ ] **Step 2: Install dependencies** Run: `yarn install` Expected: resolves and installs the new api dependencies without errors. - [ ] **Step 3: Create the api tsconfig** `apps/api/tsconfig.json`: ```json { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "skipLibCheck": true, "esModuleInterop": true, "resolveJsonModule": true, "verbatimModuleSyntax": true, "types": ["node"], "noEmit": true }, "include": ["src", "test"] } ``` - [ ] **Step 4: Create the env reader** `apps/api/src/env.ts`: ```ts export const env = { DATABASE_URL: process.env.DATABASE_URL ?? 'file:./data/app.db', BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET ?? 'dev-insecure-secret-change-me', BETTER_AUTH_URL: process.env.BETTER_AUTH_URL ?? 'http://localhost:3000', PORT: Number(process.env.PORT ?? 3000), }; ``` - [ ] **Step 5: Create the health route** `apps/api/src/routes/health.ts`: ```ts import { Hono } from 'hono'; import { HealthResponse } from '@solelog/shared'; export const health = new Hono(); health.get('/health', (c) => { const body: HealthResponse = { status: 'ok' }; return c.json(body); }); ``` - [ ] **Step 6: Create the app factory** `apps/api/src/app.ts`: ```ts import { Hono } from 'hono'; import { health } from './routes/health'; export function createApp(): Hono { const app = new Hono(); app.route('/', health); return app; } ``` - [ ] **Step 7: Create the server entry** `apps/api/src/index.ts`: ```ts import { serve } from '@hono/node-server'; import { createApp } from './app'; import { env } from './env'; const app = createApp(); serve({ fetch: app.fetch, port: env.PORT }, (info) => { console.log(`API listening on http://localhost:${info.port}`); }); ``` - [ ] **Step 8: Create the vitest config** `apps/api/vitest.config.ts`: ```ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'node', setupFiles: ['./test/setup.ts'], fileParallelism: false, }, }); ``` - [ ] **Step 9: Create a no-op setup (real DB setup arrives in Task 3)** `apps/api/test/setup.ts`: ```ts // Placeholder until Task 3 adds DB migration to the test setup. export {}; ``` - [ ] **Step 10: Write the failing health test** `apps/api/test/health.test.ts`: ```ts import { describe, it, expect } from 'vitest'; import { createApp } from '../src/app'; describe('GET /health', () => { it('returns { status: "ok" }', async () => { const app = createApp(); const res = await app.request('/health'); expect(res.status).toBe(200); expect(await res.json()).toEqual({ status: 'ok' }); }); }); ``` - [ ] **Step 11: Run the test to verify it passes** Run: `yarn workspace @solelog/api test` Expected: PASS (1 test). If `createApp` or `health` is missing, fix the files above. - [ ] **Step 12: Commit** ```bash git add apps/api yarn.lock git commit -m "feat(api): Hono backend skeleton with /health endpoint and test" ``` --- ## Task 3: Database — Drizzle + libsql + better-auth schema + migrations **Files:** - Create: `apps/api/src/db/schema.ts` - Create: `apps/api/src/db/client.ts` - Create: `apps/api/drizzle.config.ts` - Create: `apps/api/src/db/migrate.ts` - Modify: `apps/api/test/setup.ts` - Test: `apps/api/test/db.test.ts` **Interfaces:** - Produces: `db` (drizzle client) from `apps/api/src/db/client.ts`; the better-auth tables `user`, `session`, `account`, `verification` from `apps/api/src/db/schema.ts`; `runMigrations()` from `apps/api/src/db/migrate.ts` (consumed by the test setup and the Docker start). - [ ] **Step 1: Define the better-auth Drizzle schema** `apps/api/src/db/schema.ts` (this is better-auth's standard core schema for SQLite): ```ts import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; export const user = sqliteTable('user', { id: text('id').primaryKey(), name: text('name').notNull(), email: text('email').notNull().unique(), emailVerified: integer('email_verified', { mode: 'boolean' }).notNull().default(false), image: text('image'), createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), }); export const session = sqliteTable('session', { id: text('id').primaryKey(), expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(), token: text('token').notNull().unique(), createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), ipAddress: text('ip_address'), userAgent: text('user_agent'), userId: text('user_id') .notNull() .references(() => user.id, { onDelete: 'cascade' }), }); export const account = sqliteTable('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: integer('access_token_expires_at', { mode: 'timestamp' }), refreshTokenExpiresAt: integer('refresh_token_expires_at', { mode: 'timestamp' }), scope: text('scope'), password: text('password'), createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), }); export const verification = sqliteTable('verification', { id: text('id').primaryKey(), identifier: text('identifier').notNull(), value: text('value').notNull(), expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(), createdAt: integer('created_at', { mode: 'timestamp' }), updatedAt: integer('updated_at', { mode: 'timestamp' }), }); ``` > **Verification (do this during execution):** run `npx @better-auth/cli@latest generate --config src/auth.ts` after Task 4 and confirm the generated schema matches the columns above. If better-auth's installed version differs, adopt the generated schema (it is authoritative). - [ ] **Step 2: Create the drizzle client** `apps/api/src/db/client.ts`: ```ts import { drizzle } from 'drizzle-orm/libsql'; import { createClient } from '@libsql/client'; import { env } from '../env'; import * as schema from './schema'; const client = createClient({ url: env.DATABASE_URL }); export const db = drizzle(client, { schema }); ``` - [ ] **Step 3: Create the drizzle-kit config** `apps/api/drizzle.config.ts`: ```ts import { defineConfig } from 'drizzle-kit'; export default defineConfig({ schema: './src/db/schema.ts', out: './drizzle', dialect: 'sqlite', dbCredentials: { url: process.env.DATABASE_URL ?? 'file:./data/app.db', }, }); ``` - [ ] **Step 4: Generate the initial migration** Run: `yarn workspace @solelog/api db:generate` Expected: a `apps/api/drizzle/0000_*.sql` file is created containing `CREATE TABLE` statements for `user`, `session`, `account`, `verification`. - [ ] **Step 5: Create the migrate runner** `apps/api/src/db/migrate.ts`: ```ts import { drizzle } from 'drizzle-orm/libsql'; import { migrate } from 'drizzle-orm/libsql/migrator'; import { createClient } from '@libsql/client'; import { env } from '../env'; export async function runMigrations(): Promise { const client = createClient({ url: env.DATABASE_URL }); const db = drizzle(client); await migrate(db, { migrationsFolder: './drizzle' }); client.close(); } // Allow running directly: `tsx src/db/migrate.ts` if (import.meta.url === `file://${process.argv[1]}`) { runMigrations() .then(() => { console.log('Migrations applied.'); process.exit(0); }) .catch((err) => { console.error(err); process.exit(1); }); } ``` - [ ] **Step 6: Wire migrations into the test setup** Replace `apps/api/test/setup.ts` with: ```ts import { beforeAll } from 'vitest'; import { rmSync, mkdirSync } from 'node:fs'; // Use a dedicated, freshly-migrated file DB for the test run. process.env.DATABASE_URL = 'file:./.tmp/test.db'; process.env.BETTER_AUTH_SECRET = 'test-secret'; process.env.BETTER_AUTH_URL = 'http://localhost:3000'; beforeAll(async () => { rmSync('./.tmp', { recursive: true, force: true }); mkdirSync('./.tmp', { recursive: true }); const { runMigrations } = await import('../src/db/migrate'); await runMigrations(); }); ``` - [ ] **Step 7: Ignore the test/db scratch dirs** Append to root `.gitignore`: ``` # api local data apps/api/.tmp/ apps/api/data/ ``` - [ ] **Step 8: Write the DB smoke test** `apps/api/test/db.test.ts`: ```ts import { describe, it, expect } from 'vitest'; import { db } from '../src/db/client'; import { user } from '../src/db/schema'; describe('database', () => { it('can query the migrated user table (empty)', async () => { const rows = await db.select().from(user); expect(rows).toEqual([]); }); }); ``` - [ ] **Step 9: Run tests to verify they pass** Run: `yarn workspace @solelog/api test` Expected: PASS (health + db). The migration runs against `./.tmp/test.db` in setup; the query returns an empty array. - [ ] **Step 10: Commit** ```bash git add apps/api .gitignore yarn.lock git commit -m "feat(api): Drizzle + libsql DB layer with better-auth schema and migrations" ``` --- ## Task 4: better-auth wired into Hono (sign-up / sign-in) **Files:** - Create: `apps/api/src/auth.ts` - Modify: `apps/api/src/app.ts` - Test: `apps/api/test/auth.test.ts` (sign-up + sign-in only; protected route in Task 5) **Interfaces:** - Consumes: `db` and `schema` (Task 3). - Produces: `auth` (better-auth instance) from `apps/api/src/auth.ts`; mounted handler at `ALL /api/auth/*`. - [ ] **Step 1: Create the better-auth config** `apps/api/src/auth.ts`: ```ts import { betterAuth } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; import { bearer } from 'better-auth/plugins'; import { db } from './db/client'; import * as schema from './db/schema'; import { env } from './env'; export const auth = betterAuth({ secret: env.BETTER_AUTH_SECRET, baseURL: env.BETTER_AUTH_URL, trustedOrigins: [env.BETTER_AUTH_URL, 'http://localhost:3000'], database: drizzleAdapter(db, { provider: 'sqlite', schema }), emailAndPassword: { enabled: true, requireEmailVerification: false, }, plugins: [bearer()], }); ``` - [ ] **Step 2: Mount the auth handler in the app** Modify `apps/api/src/app.ts` to: ```ts import { Hono } from 'hono'; import { health } from './routes/health'; import { auth } from './auth'; export function createApp(): Hono { const app = new Hono(); app.route('/', health); app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw)); return app; } ``` - [ ] **Step 3: Write the failing sign-up / sign-in test** `apps/api/test/auth.test.ts`: ```ts import { describe, it, expect } from 'vitest'; import { createApp } from '../src/app'; const json = { 'content-type': 'application/json' }; describe('auth', () => { it('signs a user up and signs them in, returning a bearer token', async () => { const app = createApp(); const creds = { email: 'worker@example.com', password: 'sterk-wachtwoord-123', name: 'Worker' }; const signup = await app.request('/api/auth/sign-up/email', { method: 'POST', headers: json, body: JSON.stringify(creds), }); expect(signup.status).toBe(200); const signin = await app.request('/api/auth/sign-in/email', { method: 'POST', headers: json, body: JSON.stringify({ email: creds.email, password: creds.password }), }); expect(signin.status).toBe(200); const token = signin.headers.get('set-auth-token'); expect(token).toBeTruthy(); }); }); ``` - [ ] **Step 4: Run the test to verify it passes** Run: `yarn workspace @solelog/api test test/auth.test.ts` Expected: PASS. The `set-auth-token` response header is the bearer plugin's token. If the header is absent, confirm the `bearer()` plugin is registered and check the installed better-auth docs for the token-exposure header name; adjust the assertion to match. - [ ] **Step 5: Commit** ```bash git add apps/api yarn.lock git commit -m "feat(api): mount better-auth (email+password + bearer) on /api/auth" ``` --- ## Task 5: Protected route + full auth round-trip (the "done when") **Files:** - Create: `apps/api/src/routes/me.ts` - Modify: `apps/api/src/app.ts` - Test: `apps/api/test/me.test.ts` **Interfaces:** - Consumes: `auth` (Task 4), `MeResponse` from `@solelog/shared`. - Produces: `GET /api/me` — returns `401` without a valid token, `200` with `{ user }` when the bearer token is valid. - [ ] **Step 1: Create the protected route** `apps/api/src/routes/me.ts`: ```ts import { Hono } from 'hono'; import type { MeResponse } from '@solelog/shared'; import { auth } from '../auth'; export const me = new Hono(); me.get('/api/me', async (c) => { const session = await auth.api.getSession({ headers: c.req.raw.headers }); if (!session) { return c.json({ error: 'Unauthorized' }, 401); } const body: MeResponse = { user: { id: session.user.id, email: session.user.email, name: session.user.name, }, }; return c.json(body); }); ``` - [ ] **Step 2: Mount the route** Modify `apps/api/src/app.ts` to add the `me` route (full file): ```ts import { Hono } from 'hono'; import { health } from './routes/health'; import { me } from './routes/me'; import { auth } from './auth'; export function createApp(): Hono { const app = new Hono(); app.route('/', health); app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw)); app.route('/', me); return app; } ``` - [ ] **Step 3: Write the failing round-trip test** `apps/api/test/me.test.ts`: ```ts import { describe, it, expect } from 'vitest'; import { createApp } from '../src/app'; const json = { 'content-type': 'application/json' }; describe('GET /api/me', () => { it('rejects an unauthenticated request', async () => { const app = createApp(); const res = await app.request('/api/me'); expect(res.status).toBe(401); }); it('returns the user for a valid bearer token (sign-up -> sign-in -> me)', async () => { const app = createApp(); const creds = { email: 'me@example.com', password: 'sterk-wachtwoord-123', name: 'Me' }; await app.request('/api/auth/sign-up/email', { method: 'POST', headers: json, body: JSON.stringify(creds), }); const signin = await app.request('/api/auth/sign-in/email', { method: 'POST', headers: json, body: JSON.stringify({ email: creds.email, password: creds.password }), }); const token = signin.headers.get('set-auth-token'); const res = await app.request('/api/me', { headers: { authorization: `Bearer ${token}` }, }); expect(res.status).toBe(200); const body = await res.json(); expect(body.user.email).toBe(creds.email); }); }); ``` - [ ] **Step 4: Run the test to verify it passes** Run: `yarn workspace @solelog/api test` Expected: PASS (health, db, auth, me — all green). This proves the Phase 0 "done when" criterion in code. - [ ] **Step 5: Commit** ```bash git add apps/api git commit -m "feat(api): add protected GET /api/me and full auth round-trip test" ``` --- ## Task 6: Dockerize the backend + compose + run docs **Files:** - Create: `apps/api/Dockerfile` - Create: `apps/api/.env.example` - Create: `docker-compose.yml` (repo root) - Create: `apps/api/.dockerignore` - Modify: `docs/README.md` (run instructions — create if absent) **Interfaces:** - Produces: `docker compose up` serving the API on `localhost:3000` with a persisted SQLite volume and migrations applied at start. - [ ] **Step 1: Create the env example** `apps/api/.env.example`: ``` DATABASE_URL=file:./data/app.db BETTER_AUTH_SECRET=change-me-to-a-long-random-string BETTER_AUTH_URL=http://localhost:3000 PORT=3000 ``` - [ ] **Step 2: Create the dockerignore** `apps/api/.dockerignore`: ``` node_modules .tmp data drizzle/meta ``` - [ ] **Step 3: Create the Dockerfile** `apps/api/Dockerfile` (multi-arch friendly; runs migrations then starts): ```dockerfile FROM node:22-alpine AS base RUN corepack enable WORKDIR /repo # Copy workspace manifests for cached install COPY package.json yarn.lock .yarnrc.yml ./ COPY .yarn/ ./.yarn/ COPY packages/shared/package.json ./packages/shared/package.json COPY apps/api/package.json ./apps/api/package.json # Copy the other workspace manifests too, so the workspace graph matches the # lockfile (otherwise Yarn errors on a mismatch). `focus` still installs ONLY # the @solelog/api subtree, so mobile/web deps are not pulled. COPY apps/mobile/package.json ./apps/mobile/package.json COPY apps/web/package.json ./apps/web/package.json RUN yarn workspaces focus @solelog/api # Copy sources COPY packages/shared/ ./packages/shared/ COPY apps/api/ ./apps/api/ WORKDIR /repo/apps/api ENV NODE_ENV=production EXPOSE 3000 # Apply migrations, then start the server CMD ["sh", "-c", "yarn db:migrate && yarn start"] ``` > **Note:** `yarn workspaces focus` requires the `plugin-workspace-tools` (bundled with Yarn 4). If it is unavailable, substitute `RUN yarn install --immutable`. - [ ] **Step 4: Create the compose file** `docker-compose.yml` (repo root): ```yaml services: api: build: context: . dockerfile: apps/api/Dockerfile ports: - '3000:3000' environment: DATABASE_URL: file:/data/app.db BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:-change-me-to-a-long-random-string} BETTER_AUTH_URL: http://localhost:3000 PORT: '3000' volumes: - solelog_db:/data volumes: solelog_db: ``` - [ ] **Step 5: Build and start the stack** Run: `docker compose up --build -d` Expected: the `api` service builds and starts; `docker compose logs api` shows "Migrations applied." then "API listening on http://localhost:3000". - [ ] **Step 6: Smoke-test the running container** Run: `curl -s http://localhost:3000/health` Expected: `{"status":"ok"}` Run (full round-trip against the container): ```bash curl -s -X POST http://localhost:3000/api/auth/sign-up/email \ -H 'content-type: application/json' \ -d '{"email":"smoke@example.com","password":"sterk-wachtwoord-123","name":"Smoke"}' TOKEN=$(curl -s -D - -o /dev/null -X POST http://localhost:3000/api/auth/sign-in/email \ -H 'content-type: application/json' \ -d '{"email":"smoke@example.com","password":"sterk-wachtwoord-123"}' \ | grep -i '^set-auth-token:' | awk '{print $2}' | tr -d '\r') curl -s http://localhost:3000/api/me -H "authorization: Bearer $TOKEN" ``` Expected: the final call returns `{"user":{...,"email":"smoke@example.com"}}`, confirming the token authorises `/api/me`. - [ ] **Step 7: Tear down** Run: `docker compose down` Expected: container stops; the `solelog_db` volume persists for next time. - [ ] **Step 8: Write run instructions** Create `docs/README.md`: ```markdown # SoleLog docs - [Roadmap & project overview](./roadmap.md) - [Plans](./plans/) ## Running the backend (Phase 0) Local (dev): \`\`\`bash yarn install cp apps/api/.env.example apps/api/.env # edit BETTER_AUTH_SECRET yarn workspace @solelog/api db:migrate yarn workspace @solelog/api dev \`\`\` Docker (whole stack): \`\`\`bash docker compose up --build # health: curl http://localhost:3000/health \`\`\` ``` - [ ] **Step 9: Commit** ```bash git add apps/api/Dockerfile apps/api/.env.example apps/api/.dockerignore docker-compose.yml docs/README.md git commit -m "feat(api): dockerize backend with compose, migrations-on-start, and run docs" ``` --- ## Definition of Done (Phase 0) - `yarn workspace @solelog/api test` is green (health, db, auth, me). - `docker compose up --build` serves `/health` and the sign-up → sign-in → `/api/me` round-trip works against the container. - The inherited `apps/mobile` and `apps/web` are untouched. - All work committed; `docs/roadmap.md` and this plan are tracked. ## Notes for the executor (version-drift safety) better-auth, Drizzle, and drizzle-kit evolve quickly. Three checkpoints where the installed version is authoritative over this plan's sample code: 1. **Schema (Task 3):** confirm with `npx @better-auth/cli@latest generate`. 2. **Bearer token header (Tasks 4–5):** the assertion reads `set-auth-token`; verify against the installed bearer-plugin docs. 3. **drizzle-kit config (Task 3):** if `dialect: 'sqlite'` rejects the libsql `file:` URL, switch to `dialect: 'turso'` with `dbCredentials.url`. The TDD loop (write test → run → fix) will surface any mismatch immediately. ```