Lattice is a self-hosted personal knowledge system. It mirrors an Obsidian vault from S3 and uses OpenCode to answer questions grounded in the mirrored files. Chat history is stored in SQLite.
Obsidian / Remotely Save -> AWS S3 -> sync-worker -> vault mirror
|
opencode-query
|
Next.js web
|
Cloudflare Tunnel + Access
The scheduler triggers S3 sync at a fixed interval. OpenCode reads the mirror directly; no embedding model or search index is required. Only the web application publishes a host port. OpenCode query and sync-worker are internal services.
apps/web/: Next.js UI, authenticated API routes, SQLite chat persistenceservices/opencode-query/: grounded answers, folder listingservices/sync-worker/: read-only S3 sync, run status and logsservices/scheduler/: periodic sync triggerpackages/model-catalog/: fixed model and provider constantsshowcase-website/: separate marketing websiteinfra/: Docker Compose, AWS IAM, Cloudflare and systemd configuration
Use Node 24 and pnpm 12.6.0 (pinned by .nvmrc and packageManager). Local pnpm commands use the sfw wrapper. All projects share the root pnpm-lock.yaml.
sfw pnpm install --frozen-lockfile
sfw pnpm check
sfw pnpm buildFor the full stack:
- Copy
.env.exampleto.env. - Configure the S3 bucket, prefix, region, and read-only AWS credentials.
- Set
OPENROUTER_API_KEYfor API-backed answers. - Set
WEB_AUTH_MODE=devandWEB_DEV_USER_EMAIL=you@example.comfor local use. - Run
make upand openhttp://localhost:3000(or your configuredWEB_PORT).
LATTICE_DATA_ROOT is resolved relative to infra/docker/docker-compose.yml. Its default ../../data/runtime points to repository-local runtime storage.
For web-only iteration, keep backend containers running and override their URLs with reachable local endpoints if needed:
sfw pnpm --filter lattice-web devBackend containers expose ports only on the Docker network. Running only the web process on the host does not make those services reachable automatically.
All questions use GPT-6 Luna (openai/gpt-6-luna) through OpenRouter. Set OPENROUTER_API_KEY; there are no model or billing-route pickers. ChatGPT subscription authentication and OPENCODE_MODEL overrides are no longer supported.
Existing threads migrate to Luna/OpenRouter for future questions. Historical answers retain their original model and route metadata. Old OAuth files are no longer mounted or used by Lattice; existing files on disk are left untouched.
Luna uses Chat Completions with reasoning disabled so it can call OpenCode's file tools. Both the main and helper model are pinned to Luna in the worker configuration.
| Variable | Purpose |
|---|---|
LATTICE_PUBLIC_URL |
External UI hostname |
LATTICE_DATA_ROOT |
Host root for persistent runtime directories |
CHAT_DB_PATH |
Chat SQLite database path inside the web container |
WEB_AUTH_MODE |
dev, cloudflare, or auto |
WEB_DEV_USER_EMAIL |
Development identity; optional fallback with auto |
OPENROUTER_API_KEY |
Required API key for GPT-6 Luna through OpenRouter |
OPENCODE_QUERY_TIMEOUT_MS |
Inactivity timeout; default 120000, 0 disables it |
OPENCODE_PROMPT_HEARTBEAT_MS |
Worker heartbeat interval; default 15000 |
OPENCODE_WORKER_SHUTDOWN_GRACE_MS |
Grace period before process-group force-kill; default 5000 |
SYNC_S3_BUCKET / SYNC_S3_PREFIX |
S3 vault source |
SYNC_AWS_REGION |
AWS region |
SYNC_DELETE |
Whether files removed from S3 are deleted from the local mirror |
SYNC_INTERVAL_SECONDS |
Scheduled sync interval; default 300 |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY |
Read-only, bucket/prefix-scoped credentials |
CLOUDFLARE_TUNNEL_TOKEN |
Optional Cloudflare Tunnel token |
Use WEB_AUTH_MODE=cloudflare behind Cloudflare Access. auto intentionally allows the configured development fallback when Access headers are absent. Keep backend services internal and restrict direct web-port access appropriately.
Persistent directories under LATTICE_DATA_ROOT:
vault/: read-only S3 mirror source for answerschat/: SQLite chat historystatus/: sync status JSONlogs/: per-run sync logs
The scheduler posts to sync-worker, which runs scripts/sync-vault.sh, records changed-file counts and completion status, and reports OpenCode health. Manual sync uses the same pipeline.
AWS permissions should permit only s3:ListBucket for the configured prefix and s3:GetObject for its objects. See infra/aws/readonly-iam-policy.json.
See deployment and Cloudflare setup. Docker builds use Node 24, frozen pnpm installs, and an exactly matched OpenCode CLI/SDK pair. Updating either OpenCode package requires updating the other and testing native server startup/shutdown.
Before upgrading, back up chat/, status/, and any irreplaceable vault content. Existing chats from the retired QMD engine keep their messages and migrate to OpenCode/OpenRouter for future questions. Old QMD index/cache directories are no longer mounted; their contents are not deleted automatically.
After upgrading from a version containing QMD, remove the orphaned service container using the deployment command:
docker compose --env-file .env -f infra/docker/docker-compose.yml up --build -d --remove-orphansThis does not delete old bind-mounted index/cache directories. Start with only the main Compose file to remove the old subscription authentication mount.
sfw pnpm check
sfw pnpm build
sfw pnpm audit
make lint-shell
docker compose --env-file .env.example -f infra/docker/docker-compose.yml config --quietCI runs type checks, unit/integration tests, production builds, and container builds. SQLite tests use disposable databases; OpenCode integration tests use isolated configuration and make no paid model calls. Actual provider answers still require a configured API key and a manual query checking streaming, citations, file tools, and cancellation.
Use make logs, make ps, and scripts/healthcheck.sh for diagnostics. For provider failures, verify OPENROUTER_API_KEY, OpenRouter credits, and GPT-6 Luna availability. For sync failures, inspect AWS permissions and sync-worker logs.