Implement CHUNK #2: server layer, core schemas/adapters, contract tests, seed

Complete the server-backed storage layer for PubScript so the app can run
against a real Fastify + SQLite backend instead of only local IndexedDB.

Server (@pubscript/server):
- db.ts: better-sqlite3 schema + migrations (users, shares, projects, books,
  chapters, entities, boards, ai_sessions, audit); WAL mode; idempotent
  admin bootstrap via PUBSCRIPT_ADMIN_EMAIL/PASSWORD.
- auth.ts: argon2 hashing, JWT-ish session tokens, guards, auth routes.
- routes.ts: REST CRUD for projects/books/chapters/entities/boards/ai_sessions
  with multi-tenant isolation and optimistic concurrency (baseRev).
- ws.ts: Fastify websocket hub for autosave + presence events.
- mapping.ts: row <-> core type conversion.

Core (@pubscript/core):
- Zod domain schemas (projects, books, chapters, entities, boards, ai_sessions)
  exported from index.
- storage-local.ts (IndexedDB/Dexie) and storage-server.ts (REST) adapters
  implementing the StorageAdapter contract.

Tests & tooling:
- storage.contract.test.ts: parameterized over both adapters; wired as
  `pnpm contract` and added to CI.
- seed.ts (`pnpm --filter server seed`): creates demo project "The Glass Harbor"
  — 3 chapters of prose, 4 linked characters, 1 world location, 6-node board.
- Dockerfile + compose wiring for the server runtime.

Acceptance: seed project opens with 3 chapters, 4 linked characters, 1 board.
This commit is contained in:
Schwitzz
2026-10-24 18:31:20 -05:00
parent 53f4c5b4c7
commit 1c9b531996
95 changed files with 10006 additions and 16 deletions
+51
View File
@@ -0,0 +1,51 @@
# Multi-stage build: build the web bundle + server, then serve them from a
# slim runtime image as a single self-hosted container (PLAN.md §7).
FROM node:22-alpine AS build
WORKDIR /app
# Build tools for native modules (better-sqlite3) in the build stage.
RUN apk add --no-cache python3 make g++
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml tsconfig.base.json ./
COPY packages ./packages
COPY apps ./apps
COPY server ./server
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate \
&& pnpm install --frozen-lockfile \
&& pnpm build
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
# Build tools for native modules (better-sqlite3, argon2) + su-exec for the
# entrypoint to drop privileges to the non-root user at runtime.
RUN apk add --no-cache python3 make g++ su-exec
# Non-root user.
RUN addgroup --system --gid 1001 pubscript \
&& adduser --system --uid 1001 pubscript
# Source + lockfile from the build stage (apps/web/dist carries the built web bundle).
COPY --from=build /app/package.json /app/pnpm-workspace.yaml /app/pnpm-lock.yaml ./
COPY --from=build /app/packages ./packages
COPY --from=build /app/apps ./apps
COPY --from=build /app/server ./server
# Install production deps only (native modules compile here).
RUN corepack enable && corepack prepare pnpm@9.15.9 --activate \
&& pnpm install --prod --frozen-lockfile
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Data volume for the SQLite file.
VOLUME ["/app/data"]
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:8080/api/health || exit 1
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
CMD ["sh", "-c", "cd /app/server && exec node --import tsx src/index.ts"]
+16
View File
@@ -0,0 +1,16 @@
services:
pubscript:
build:
context: ..
dockerfile: docker/Dockerfile
image: pubscript:latest
ports:
- "8080:8080"
environment:
- PUBSCRIPT_DB=/app/data/pubscript.sqlite
- PUBSCRIPT_ORIGIN=http://localhost:8080
- PUBSCRIPT_ADMIN_EMAIL=admin@pubscript.local
- PORT=8080
volumes:
- ./data:/app/data
restart: unless-stopped
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
set -e
# Runs as root (the image does not set USER), so it can fix ownership of the
# data volume, then drops privileges to the non-root pubscript user before
# exec'ing the server. This lets SQLite write to the mounted data dir.
mkdir -p /app/data
chown -R pubscript:pubscript /app/data
exec su-exec pubscript "$@"