Skip to main content
Glama
README.md
# Stillcurrent

Stillcurrent keeps the architecture docs and diagrams of your software up to date: LLM agents write and update them over MCP and a REST API, and engineers and architects read them. Documents are Markdown with first-class Mermaid diagrams. Users sign in, keep their documents in PostgreSQL, group them into projects, and read them with IBM Plex typography, a contents sidebar, callouts, highlighted code and themed, zoomable diagrams (with special routing for C4). Scripts use a REST API; LLM clients connect over MCP.

- **Stack:** Next.js 16 (App Router), React 19, TypeScript, PostgreSQL 17 with Drizzle ORM, Better Auth, Zod, marked + DOMPurify + highlight.js, Mermaid 11.16, CodeMirror 6, Vitest and Playwright.
- **Contracts:** `docs/GUIDE.md` is the authoring guide served at `/guide` (and raw at `/guide.md`); everything it promises renders. `PLAN.md` is the build plan; `CLAUDE.md` holds the coding rules.

## Getting started

Requirements: Node 22 LTS, pnpm 10 (`corepack enable`), Docker.

```bash
pnpm install
cp .env.example .env.local          # then set BETTER_AUTH_SECRET (openssl rand -base64 32)
pnpm db:up                          # Postgres 17 in Docker, with the stillcurrent and stillcurrent_test databases
pnpm db:migrate
pnpm db:seed                        # dev@example.com / dev-password-123 and the welcome document
pnpm dev                            # http://localhost:3000
```

To run the whole stack in Docker instead (Postgres, migrations and the app on http://localhost:8080), put a `BETTER_AUTH_SECRET` in a `.env` file next to `docker-compose.yml` (there is no default, so no two installations share one), then:

```bash
docker compose up -d --build
```

The local Compose file has a development database password and open sign-up; never expose it. See [Deployment](#deployment) for production.

## Scripts

| Command | What it does |
|---|---|
| `pnpm dev` | Dev server (Turbopack) |
| `pnpm build` / `pnpm start` | Production build and server |
| `pnpm check` | Lint, type check and unit tests; run before every commit |
| `pnpm lint` / `pnpm typecheck` / `pnpm test` | The three parts of `check` |
| `pnpm test:e2e` | Playwright. Locally it reuses a running dev server; `CI=1 PORT=3400 pnpm test:e2e` runs against `pnpm build` output |
| `pnpm db:up` / `pnpm db:down` | Start or stop the Postgres container |
| `pnpm db:generate` | Create a migration after a schema change (review the SQL, never edit an applied migration) |
| `pnpm db:migrate` | Apply migrations |
| `pnpm db:studio` | Drizzle Studio |
| `pnpm db:seed` | Development user and welcome document |

Unit tests are co-located (`*.test.ts(x)`). Repository and service tests run against the `stillcurrent_test` database (`DATABASE_URL_TEST`), one file at a time.

## Architecture

```
src/app/         routes: pages, layouts, server actions, route handlers (/api/v1, /api/mcp, /api/health)
src/components/  React components by feature (shell, viewer, diagrams, workspace, library, projects, settings)
src/lib/         pure, isomorphic logic: Markdown pipeline, Mermaid helpers and C4 router, preferences, formatting
src/server/      server-only: db (Drizzle schema + migrations), repositories and services per domain,
                 auth, http (problem+json, tokens, rate limits), mcp, logging
src/proxy.ts     request id, security headers with a per-request CSP nonce, sign-in gate
```

- **Layers:** `app → components → lib` and `app → server`. Only repositories touch the database; services enforce authorisation (every query is scoped to the signed-in user, a share link, or an API token and its project restriction) and throw typed domain errors.
- **Same services everywhere:** the UI (server actions), the REST API (`/api/v1`, OpenAPI at `/api/v1/openapi.json`) and the MCP server (`/api/mcp`) call the same service functions.
- **Rendering:** `lib/markdown/render.ts` turns Markdown into sanitized HTML on the server (and in the editor's live preview). Documents are stored as Markdown, never as HTML. Mermaid runs only in the browser (`securityLevel: 'strict'`) and loads only when a document has diagrams; the server's `check_markdown` parses diagrams in a worker thread with its own JSDOM.
- **Concurrency:** every update carries `expectedVersion`; a stale write gets a conflict instead of overwriting.
- **Security:** nonce-based CSP with `strict-dynamic`, no `eval` in production, `frame-ancestors 'none'`, HSTS on HTTPS, hashed share links and API tokens (shown once), rate limits (60 requests per minute per token; sign-in 10 attempts per account and 30 per address per minute, sign-up 10 per address per 10 minutes, password checks 10 per user per 10 minutes). The address comes from the last `X-Forwarded-For` entry (or `X-Real-IP`) that the reverse proxy sets; without a proxy (a loopback address), only the per-account and per-user limits apply.
- **Logs:** one JSON line per event on stdout/stderr with the request id. Logs never contain document text, Markdown, tokens or passwords; MCP tool calls log only the tool name, outcome and duration.

## Environment variables

Validated at startup by `src/env.ts`; the list with comments is in `.env.example`.

| Variable | Required | Notes |
|---|---|---|
| `DATABASE_URL` | yes | `postgres://user:password@host:5432/db` |
| `DATABASE_URL_TEST` | tests | Database the unit tests may wipe |
| `BETTER_AUTH_SECRET` | yes | At least 32 characters: `openssl rand -base64 32` |
| `BETTER_AUTH_URL` | yes | The app's public URL (same as `APP_URL`) |
| `APP_URL` | yes | Public base URL used in share links, API and MCP responses. `https://` turns on HSTS and `upgrade-insecure-requests` |
| `SIGNUP_ENABLED` | no | Defaults to `true` in development and `false` in production |
| `LOG_LEVEL` | no | `info` (default), `warn` or `error` |
| `MERMAID_WORKER_MODULE_DIR` | no | Folder whose `node_modules` holds `mermaid` and `jsdom` for `check_markdown` (set in the Docker image) |
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASSWORD`, `SMTP_FROM` | no | Reserved for password-reset email; leave `SMTP_HOST` empty to disable |

## Deployment

The `Dockerfile` builds a standalone Next.js server that runs as the unprivileged `node` user with a healthcheck on `/api/health`, plus a `migrate` target that applies migrations and exits. `docker-compose.prod.yml` wires Postgres, the migration step and the app; every secret is required.

1. Create a server (or managed Postgres) in an **EU region**; the data is personal data under the GDPR.
2. Put a `.env` next to `docker-compose.prod.yml`:

   ```bash
   POSTGRES_USER=stillcurrent
   POSTGRES_PASSWORD=<long random string>
   BETTER_AUTH_SECRET=<openssl rand -base64 32>
   APP_URL=https://docs.example.com
   SIGNUP_ENABLED=true        # create the first accounts, then set false and restart
   ```

3. `docker compose -f docker-compose.prod.yml up -d --build`. The `migrate` service runs before every app start, so deploying a new version is the same command.
4. Terminate TLS in front of the app (Caddy, nginx or a load balancer) and forward to `127.0.0.1:3000`. The proxy must pass the client address in `X-Forwarded-For` (Caddy and nginx's `$proxy_add_x_forwarded_for` do; the app reads the last entry) so the per-address sign-in limits work, and forward `Host` unchanged so Next's Server Action origin check and Better Auth's cookies see the public origin. Point your uptime monitor at `https://docs.example.com/api/health` (200 when the app and the database are up, 503 otherwise).

With sign-up disabled, new accounts are created by turning `SIGNUP_ENABLED` on briefly; a command-line tool for accounts and password resets is still open in `PLAN.md`.

## Backups

All state lives in PostgreSQL (documents, revisions, projects, share links, tokens, preferences, accounts).

```bash
# nightly dump (keep several days, store a copy outside the server, in the EU)
docker compose -f docker-compose.prod.yml exec -T postgres \
  pg_dump -U "$POSTGRES_USER" -Fc stillcurrent > "stillcurrent-$(date +%F).dump"

# restore into an empty database
docker compose -f docker-compose.prod.yml exec -T postgres \
  pg_restore -U "$POSTGRES_USER" -d stillcurrent --clean --if-exists < stillcurrent-2026-10-04.dump
```

Test a restore now and then. Revisions keep the last 100 versions of each document, which covers mistakes but is not a backup.

## Maintenance guides

### Adding a highlight language

1. Import the language from `highlight.js/lib/languages/<name>` in `src/lib/markdown/highlight.ts` and add it to `LANGUAGES`; add aliases to `ALIASES` and a display label to `LABELS`.
2. List it (with aliases) in `docs/GUIDE.md`, section 6. LLMs only use what the guide names.
3. Run `pnpm check`; `highlight.test.ts` fails when the guide's list and the registered languages differ.

highlight.js runs on the server for read views; the language adds nothing to the read view's JavaScript (`tests/e2e/performance.spec.ts` checks this).

### Upgrading Mermaid safely

Mermaid is pinned to 11.16.x because the C4 post-processing (`src/lib/mermaid/c4/postprocess.ts`) depends on Mermaid's C4 SVG structure (`g.person-man`, `marker-end` lines, `#444444` strokes, `aria-roledescription="c4"`).

1. Bump `mermaid` in `package.json` on a branch.
2. Run `pnpm check`: the C4 router tests, the palette contrast tests and the parse test that feeds every Mermaid block in `docs/GUIDE.md` and the welcome document through the new version.
3. Run `CI=1 PORT=3400 pnpm test:e2e` (diagram rendering, C4 routing without crossings, CSP).
4. Open the welcome document (`/dev/welcome` in development) in light and dark and look at every diagram, especially the C4 ones. Update the guide if the supported syntax changed.

### Performance budgets

`tests/e2e/performance.spec.ts` enforces them on the production build: public read view ≤ 155 KB of gzipped JavaScript, workspace read view ≤ 170 KB (React and Next alone are about 130 KB), Mermaid only on documents with diagrams, and LCP ≤ 2 s for a 2,000-word document over throttled 4G. Keep rarely used UI (dialogs, editors, diagram code) behind dynamic imports, and keep Zod out of browser code (`lib/prefs/fields.ts` explains why).

## Connecting MCP clients

LLM apps that speak the Model Context Protocol can read, check, create and update documents, manage projects, see history and share. Create a token in **Settings → API tokens** (read-only, read and write, optionally limited to some projects); **Settings → Connect an MCP client** shows copy-ready setups with your URL.

```bash
# Claude Code
claude mcp add --transport http stillcurrent https://docs.example.com/api/mcp \
  --header "Authorization: Bearer scur_…"
```

```json
// Cursor (.cursor/mcp.json)
{ "mcpServers": { "stillcurrent": { "url": "https://docs.example.com/api/mcp",
  "headers": { "Authorization": "Bearer scur_…" } } } }
```

Claude Desktop and VS Code setups are on the settings page. The server is stateless Streamable HTTP (`POST /api/mcp`), shares the REST rate limit, and tells the model to read the authoring guide, run `check_markdown` before saving, and pass `expectedVersion` on updates.

## REST API

`/api/v1` covers documents (list, search, create, read, raw Markdown, update, move between projects, trash) and projects (list, create, read, update, delete, zip export). Authenticate with `Authorization: Bearer scur_…`. The OpenAPI 3.1 description is at `/api/v1/openapi.json`; errors are RFC 9457 `application/problem+json`.

## License

Stillcurrent is licensed under the [PolyForm Noncommercial License 1.0.0](LICENSE.md). You may use, change and share it for any noncommercial purpose. Anyone you give a copy to, changed or not, must also get the license terms and the `Required Notice:` line from `LICENSE.md`. Commercial use needs a separate license from the copyright holder.