Skip to content

jwicks31/Zero-Config-Data-API

Repository files navigation

Zero-Config Data API

CI

A backend for the sites you didn't want to build a backend for.

You hand an AI a prompt and it generates a slick HTML page — then you hit a wall: there's nowhere to save data, store files, call AI, or push realtime updates. This project is that missing backend. Drop it on a single VM, point your generated frontend at it, and the endpoints just work. No database to provision, no schema to declare, no buckets to create, no auth provider to wire.

Think Firebase / PocketBase, but truly zero-config and pinned to one box.

Run it in one command

npx zero-config-data-api            # once published to npm
npx github:jwicks31/zero-config-data-api   # from source, available today

That's it — no clone, no config, no database to provision. Open http://localhost:3737 (demo), /examples.html (sample apps), or /admin (dashboard). State lives in ./.data; set PORT, DATA_DIR, ADMIN_KEY, or ANTHROPIC_API_KEY to taste — all optional.

What you get

Capability What it does
Data Schemaless collections — CRUD + bulk, filter/sort (incl. OR groups & timestamp ranges), field projection, count, aggregation (sum/avg/min/max), full-text search, optimistic-concurrency writes, and live subscriptions
Access control Per-collection public/private read & write, with field redaction for anonymous readers
Files Upload / download / list / delete — local disk by default, S3 (or any S3-compatible store) optional
AI Chat (JSON or SSE streaming) over a pluggable provider — Anthropic, any OpenAI-compatible endpoint (OpenAI, LiteLLM, Ollama…), or Amazon Bedrock
Realtime Websocket pub/sub with presence (join/leave/typing + roster), history, and an optional Redis fan-out for multi-process scale
Webhooks Outbound HTTP on data/file events — HMAC-signed, with a persisted, retried delivery queue and delivery history
Multi-tenant Optional isolated projects — one VM hosts many sites (set ADMIN_KEY)
Backup Whole-instance export / restore as one JSON file

Plus a zero-build browser SDK (/sdk.js, typed), a self-describing API (/v1 + /llms.txt) an AI can consume in one read, an admin dashboard (/admin), ops endpoints (/v1/stats, Prometheus /metrics, a /ready probe), per-IP rate limiting, and one-command deploys (npx · Docker Compose · systemd). TypeScript + Fastify, single process, SQLite + local disk by default — Postgres and S3 optional. CI, MIT.

Documentation map

Two audiences, two paths through these docs:

  • Building a frontend against it (you call the API) → Browser SDK and API below, plus the in-product reference at /docs and the one-read /llms.txt for AI agents. You never need to know which database or storage backend the operator chose — the API is identical.
  • Running / deploying it (you stand up the server) → Deploy and Configuration, including the optional Postgres and S3 backends. Everything here is set with environment variables at deploy time.

Quick start

npm install
npm run dev          # http://localhost:3737  (live reload)

Then open:

No environment variables required.

Run the test suite (spins up the server on an ephemeral port and exercises every module over HTTP + websockets):

npm test

Deploy (single VM)

Docker Compose (recommended)

docker compose up -d        # builds, runs, persists to a named volume, restarts on boot

Set ADMIN_KEY / API_KEY / ANTHROPIC_API_KEY in docker-compose.yml (all optional). Includes a /health healthcheck.

A pre-built image is published to GHCR on each vX.Y.Z release tag:

docker run -d -p 3737:3737 -v zcda-data:/data ghcr.io/jwicks31/zero-config-data-api:latest

Docker (manual)

docker build -t zero-config-data-api .
docker run -d --name zcda -p 3737:3737 -v zcda-data:/data \
  -e ANTHROPIC_API_KEY=sk-ant-...   `# optional, enables AI` \
  zero-config-data-api

All state lives in the /data volume — back it up and you've backed up everything.

Bare metal (systemd)

npm ci && npm run build
# then install the unit and start it:
sudo cp deploy/zero-config-data-api.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now zero-config-data-api

See deploy/zero-config-data-api.service for the unit (paths, env, hardening).

Browser SDK

The server ships a zero-build client at /sdk.js — the intended way for a (generated) frontend to talk to the backend. No npm, no bundler.

<script src="/sdk.js"></script>
<script>
  const api = Zero();                       // same-origin; or Zero('https://my-vm:3737', { apiKey })

  await api.data('guestbook').create({ name, message });
  await api.data('guestbook').list({ where: { done: false }, sort: '-createdAt', limit: 20 });

  const meta = await api.files.upload(fileInput.files[0]);   // -> { id, url, size, ... }

  const { text } = await api.ai.chat('Write a haiku about SQLite');
  await api.ai.stream('Tell me a story', (chunk) => append(chunk));

  const room = api.realtime('room-42');
  room.subscribe((msg) => render(msg.data));
  room.publish({ hello: 'world' });

  // Live data — every write to a collection is pushed to subscribers
  api.data('todos').subscribe((change) => {
    // { type: 'created'|'updated'|'deleted', collection, id, document? }
    refresh(change);
  });
</script>

Filter shorthand: where: { age: { op: 'gte', value: 18 } } (ops: eq ne gt gte lt lte like in nin); a bare value means equality, an array means in. Queries also take or, select, limit, offset, sort, and after (cursor). The collection client adds createMany, count, page (returns nextCursor), deleteWhere, getAcl/setAcl, getSchema/setSchema/clearSchema, getIndexes/setIndexes, and optimistic-concurrency updates (update(id, patch, { ifVersion })); the top-level client adds stats(). Failed calls throw an Error with .status and .body.

TypeScript: types are served at /sdk.d.ts — reference them for a fully typed client:

/// <reference path="./sdk.d.ts" />
const api = Zero();                  // Zero.Client
const todos = await api.data('todos').list({ where: { done: false } }); // Zero.Doc[]

Design principles

  • Zero config for the app, configurable for the operator. Every setting has a working default; the server boots with no env vars. The promise is to the API consumer (the generated site) — so an operator can swap in production backends (Postgres, S3, …) via env at deploy time without the API surface changing at all.
  • One VM, one folder — until you grow. By default all state — the SQLite database and uploaded file blobs — lives under DATA_DIR (default ./.data); backup = copy the folder. Point data at Postgres (DB_DRIVER=postgres) and/or files at S3 (STORAGE_DRIVER=s3) when you outgrow a single disk, with no change to the API the frontends call.
  • Self-describing. GET /v1 returns the capability + endpoint map, and /llms.txt is a one-read guide so an AI can wire a page to the backend zero-shot. Working in the repo? See AGENTS.md.

API

Data — schemaless collections

# Create (the "notes" collection is created on first write)
curl -X POST localhost:3737/v1/data/notes \
  -H 'content-type: application/json' \
  -d '{"title":"hello","done":false}'

curl localhost:3737/v1/data/notes              # list (newest first)

# Bulk create (one transaction)
curl -X POST localhost:3737/v1/data/notes/bulk \
  -H 'content-type: application/json' -d '[{"title":"a"},{"title":"b"}]'

# Query: filter + sort + paginate
curl 'localhost:3737/v1/data/notes?f.done=false&sort=-createdAt&limit=20'
curl 'localhost:3737/v1/data/notes?sort=-createdAt&limit=20&after=<nextCursor>' # stable cursor paging
curl 'localhost:3737/v1/data/notes?f.priority=gte:3'   # ops: eq ne gt gte lt lte like in nin
curl 'localhost:3737/v1/data/notes?f.status=in:active,pending'  # value in a set
curl 'localhost:3737/v1/data/notes?f.createdAt=gte:1700000000000' # filter on timestamps too
curl 'localhost:3737/v1/data/notes?f.done=false&or.pinned=true&or.priority=gte:5' # AND + OR group
curl 'localhost:3737/v1/data/notes?select=title,done'  # project: only these fields (+ id/timestamps)
curl 'localhost:3737/v1/data/notes/count?f.done=false' # -> { count } without the documents
curl 'localhost:3737/v1/data/orders/aggregate?op=sum&field=amount' # -> { value } (count|sum|avg|min|max)

# Full-text search (ranked, auto-indexed)
curl 'localhost:3737/v1/data/notes/search?q=hello+world'

curl localhost:3737/v1/data/notes/<id>         # read one (sends an ETag = version)
curl -X PATCH localhost:3737/v1/data/notes/<id> \
  -H 'content-type: application/json' -d '{"done":true}'   # merge
curl -X PATCH localhost:3737/v1/data/notes/<id> \
  -H 'if-match: <updatedAt>' -d '{"done":true}'   # optimistic concurrency → 409 if stale
curl -X PUT    localhost:3737/v1/data/notes/<id> -d '{...}' # replace
curl -X DELETE localhost:3737/v1/data/notes/<id>
curl -X DELETE 'localhost:3737/v1/data/notes?f.done=true'   # bulk delete by filter (?all=true to clear)
curl localhost:3737/v1/data                     # list all collections

# Declared indexes — back hot filter/sort fields with real indexes for large collections
curl -X PUT localhost:3737/v1/data/notes/indexes \
  -H 'content-type: application/json' -d '{"fields":["done","createdAt"]}'   # declarative set
curl localhost:3737/v1/data/notes/indexes        # -> { fields: [...] }

Every document gets a generated id, createdAt, and updatedAt. Pass your own id in the body to choose it.

Queries work on any field with no setup. For large collections, declare indexed fields so filters and sorts on them use a real database index (a SQLite/Postgres expression index) instead of a scan — PUT /v1/data/:collection/indexes { fields } is declarative, so it reconciles to exactly the set you send. It's key-gated and never anonymous. On Postgres the index is built CONCURRENTLY, so declaring one on a large, live collection doesn't block writes.

Collections are schemaless by default. Opt into validation per collection by declaring a JSON Schema; writes that violate it then fail with 422 and a list of problems (PATCH validates the merged result, and bulk is all-or-nothing):

curl -X PUT localhost:3737/v1/data/members/schema \
  -H 'content-type: application/json' \
  -d '{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer","minimum":0}},"required":["name"]}'

curl -X POST localhost:3737/v1/data/members -H 'content-type: application/json' -d '{"age":-1}'
#→ 422 { "error":"validation_failed", "errors":[{"field":"name","message":"..."},{"field":"age","message":"..."}] }

curl -X DELETE localhost:3737/v1/data/members/schema   # back to schemaless

Lists support both offset (?limit&offset) and cursor paging: a list response includes a nextCursor; pass it back as ?after=<cursor> to fetch the next page in stable (sort, id) order — no skips or repeats when rows are inserted mid-iteration, the way offset drifts. With the SDK: const page = await api.data('notes').page({ sort: '-createdAt', after }); then follow page.nextCursor until it's null. Back a hot sort field with a declared index and cursor paging stays fast at any size.

Via the SDK: api.data('members').setSchema(schema) / getSchema() / clearSchema().

Access control — per-collection rules

By default every collection is private: the API key (if set) governs all access. Mark a collection's read (or write) public so an anonymous frontend can reach it even on an API_KEY-locked instance, and hide sensitive fields from those anonymous reads. Managing rules always requires the key.

# Expose "posts" for anonymous reads, but redact the author's email
curl -X PUT localhost:3737/v1/data/posts/acl \
  -H "authorization: Bearer $API_KEY" -H 'content-type: application/json' \
  -d '{"read":"public","hidden":["authorEmail"]}'

curl localhost:3737/v1/data/posts          # now works without a key; authorEmail omitted

Key-holders still see every field. This works in both modes: in multi-tenant mode, a read:public (or write:public) collection is reachable anonymously by a project's non-secret project id — pass it as X-Project-Id: <prj_…> (or ?project=<prj_…> for links/downloads), e.g. Zero(base, { project: 'prj_…' }). That lets a published frontend read public content without embedding the secret project key; private collections still require it. A read:public collection's live feed is public too: Zero(base, { project }).data('posts').subscribe(…) streams changes over a read-only websocket with no secret.

Files — upload / download

curl -X POST localhost:3737/v1/files -F file=@photo.jpg   # -> { id, url, size, ... }
curl localhost:3737/v1/files                              # list metadata
curl localhost:3737/v1/files/<id> -o out.jpg              # download
curl -X DELETE localhost:3737/v1/files/<id>

Blobs land on local disk by default (under DATA_DIR). For durable or multi-instance deploys, set STORAGE_DRIVER=s3 + S3_BUCKET (works with AWS S3 or any S3-compatible store like MinIO via S3_ENDPOINT); credentials come from the standard AWS chain. The upload/download API and SDK are identical either way — see Configuration.

AI — chat (pluggable provider)

One endpoint, any backend — pick with AI_PROVIDER (anthropic · openai · bedrock; see AI providers). Returns 503 until the selected provider is configured — the rest of the server still runs without it.

# Non-streaming → { text, model, stop_reason, usage }
curl -X POST localhost:3737/v1/ai/chat \
  -H 'content-type: application/json' \
  -d '{"messages":[{"role":"user","content":"Write a haiku about SQLite"}]}'

# Streaming (Server-Sent Events): text deltas as data: frames, then event: done
curl -N -X POST localhost:3737/v1/ai/chat \
  -H 'content-type: application/json' \
  -d '{"messages":[{"role":"user","content":"hi"}],"stream":true}'

Optional body fields: system, model (defaults to a Claude model on anthropic; required for openai/bedrock), max_tokens (capped at 64000).

The provider defaults to the instance-wide AI_PROVIDER, but a project can override it — point /v1/ai/chat at its own provider/model/key/gateway:

curl -X PUT localhost:3737/v1/ai/config -H 'content-type: application/json' -d '{
  "provider": "openai", "model": "gpt-4o",
  "baseUrl": "http://localhost:4000", "apiKey": "sk-…", "headers": {"x-org": "acme"}
}'
curl localhost:3737/v1/ai/config        # redacted (keys never returned); DELETE to reset

Keys are stored server-side and never read back (GET shows hasApiKey, not the key). Key-gated and per-project; absent an override, the instance default applies.

Realtime — websocket pub/sub + presence

// Pass an optional presence identity with ?as=
const ws = new WebSocket('ws://localhost:3737/v1/realtime/room-42?as=ada');
ws.onmessage = (e) => {
  const f = JSON.parse(e.data);
  if (f.type === 'presence') console.log(f.event, f.member ?? f.members); // sync|join|leave|typing
  else console.log('peer said', f.data);                                 // { type:'message', id, channel, data, at }
};
ws.onopen = () => ws.send(JSON.stringify({ text: 'hello room' }));        // broadcast to peers
// Typing indicator (a control frame, not published):
ws.send(JSON.stringify({ $presence: { typing: true } }));

Each connection appears in the channel's presence roster (with the optional ?as= identity); peers get join/leave/typing events and a sync snapshot on connect. With the SDK: const room = api.realtime('room-42', { as: 'ada' }); room.presence(e => …); room.typing(true); await room.members();

On connect you replay the channel's recent history. Servers and non-WS clients can publish over HTTP, and you can inspect channels:

curl -X POST localhost:3737/v1/realtime/room-42 \
  -H 'content-type: application/json' -d '{"text":"from the server"}'
curl localhost:3737/v1/realtime                    # active channels + client counts
curl localhost:3737/v1/realtime/room-42/history    # recent messages
curl localhost:3737/v1/realtime/room-42/presence   # current members [{ cid, identity }]

Single-process and in-memory by default. For horizontal scale, set REDIS_URL and run multiple server processes: messages and presence fan out across them via Redis, with no change to the API or SDK.

In multi-tenant mode, the live feed of a read:public collection (data:<collection>) can be followed anonymously with just the non-secret project id ({ project }) — a read-only subscription, so a published frontend gets live updates without a secret. Other channels still require a project key.

Webhooks — outbound events

Register a URL to receive data.* / file.* events as HTTP POSTs. Deliveries are persisted and retried with exponential backoff (so a brief outage on your side doesn't drop events), and you can inspect delivery history.

# Subscribe to created/updated docs in the "orders" collection
curl -X POST localhost:3737/v1/webhooks -H 'content-type: application/json' -d '{
  "url": "https://example.com/hook",
  "events": ["data.created", "data.updated"],
  "collections": ["orders"]
}'   # -> { id, url, events, secret: "whsec_…", ... }   (secret shown once)

curl localhost:3737/v1/webhooks                 # list (without secrets)
curl localhost:3737/v1/webhooks/<id>/deliveries # recent attempts + status
curl -X POST localhost:3737/v1/webhooks/<id>/rotate-secret
curl -X DELETE localhost:3737/v1/webhooks/<id>

Event types use a resource.action shape (data.created, data.updated, data.deleted, file.created, file.deleted); subscribe to exact types, a prefix (data.*), or all (*), and optionally filter by collections. Each delivery is a JSON POST { id, event, createdAt, project, data } with headers:

Header Meaning
X-Zero-Event the event type
X-Zero-Delivery unique delivery id
X-Zero-Timestamp send time (ms)
X-Zero-Signature sha256=<hex> — HMAC-SHA256 of "<timestamp>.<rawBody>" keyed by the hook's secret

Verify by recomputing the HMAC over `${timestamp}.${rawBody}` with your stored secret and comparing to X-Zero-Signature. Management is key-gated (and per-project in multi-tenant); since webhooks make outbound requests, run with a key and restrict egress if the instance is internet-exposed.

Verifiable identity (JWT / OIDC)

Optionally verify an end-user token so calls carry a trusted identity. Configure either a shared HS256 secret (AUTH_JWT_SECRET) or an OIDC JWKS endpoint (AUTH_JWKS_URL, with AUTH_JWT_ISSUER / AUTH_JWT_AUDIENCE). A caller presents the token as Authorization: Bearer <jwt>, an X-User-Token header (so it can sit beside a project key), or ?token= (for websockets):

curl localhost:3737/v1/me -H "Authorization: Bearer $JWT"   # -> { user: { sub, claims } }

A verified token is echoed at /v1/me, becomes the trusted realtime presence identity (overriding ?as=), and on an API_KEY-locked instance stands in for the key — so end-users authenticate with their own IdP tokens instead of sharing a secret. Zero(base, { token }) sends it for you. An invalid/expired token is a 401; identity is off until configured. (Row-level per-user authorization is the natural next step and isn't included yet.)

Multi-tenant projects

By default the server runs in open mode: one implicit project, no auth — exactly the zero-config experience. Set ADMIN_KEY to switch to multi-tenant mode, where one VM hosts many isolated sites:

  • Each project has its own key and a fully isolated slice of every collection, file, and realtime channel.
  • Data requests must carry a project key (Authorization: Bearer pk_…, or ?key= for websockets). The SDK takes it as Zero(base, { apiKey: '<project key>' }).
  • Public access without a secret: a project's collections marked read/write public are reachable by the non-secret project id alone — pass X-Project-Id: prj_… (or ?project=), i.e. Zero(base, { project: 'prj_…' }). Ideal for a published frontend that reads public content; private collections still require the key.
  • The ADMIN_KEY also acts as the access key for the shared/default space, so the operator (and the dashboard's “— shared —” context) can read/write shared data alongside the isolated apps.
  • Projects are managed via the admin API, authenticated with ADMIN_KEY:
# Create a project (returns its key — hand it to that site)
curl -X POST localhost:3737/v1/admin/projects \
  -H "Authorization: Bearer $ADMIN_KEY" -H 'content-type: application/json' \
  -d '{"name":"my-blog"}'                     # -> { id, name, key: "pk_…", createdAt }

curl -H "Authorization: Bearer $ADMIN_KEY" localhost:3737/v1/admin/projects     # list
curl -X POST -H "Authorization: Bearer $ADMIN_KEY" \
  localhost:3737/v1/admin/projects/<id>/rotate-key   # new key; old one stops working, data kept
curl -X DELETE -H "Authorization: Bearer $ADMIN_KEY" \
  localhost:3737/v1/admin/projects/<id>      # deletes the project AND its data

GET /v1 reports the current mode. Existing single-tenant databases migrate in place on boot (all data lands under the default project).

Backup & restore

Whole-instance snapshot as one JSON document (all documents + base64 file blobs). Gated: these endpoints only work when API_KEY is set — they expose and overwrite everything, so they stay dormant (403) on an open instance.

# Snapshot the entire backend to a file
curl -H "Authorization: Bearer $API_KEY" localhost:3737/v1/export > backup.json

# Restore it (upserts documents and files by id) onto any instance
curl -X POST localhost:3737/v1/import \
  -H "Authorization: Bearer $API_KEY" -H 'content-type: application/json' \
  --data-binary @backup.json

Configuration

Operator docs. Everything below is for whoever runs the server. None of it changes the API your frontends call — it's how you tune and scale a deployment. Every value is optional: the server boots zero-config on SQLite + local disk. See .env.example for the complete list. A .env in the working directory is auto-loaded at startup; real environment variables win.

Core & access

Variable Default Purpose
PORT / HOST 3737 / 0.0.0.0 Listen address
DATA_DIR ./.data Where SQLite + local file blobs live (one folder = one backup)
API_KEY (unset) If set, locks the whole instance with one bearer key
ADMIN_KEY (unset) If set, enables multi-tenant projects (manage via /v1/admin)
CORS_ORIGIN (unset → *) Comma-separated browser origin allowlist; unset = open CORS
AUTH_JWT_SECRET (unset) HS256 secret to verify end-user identity tokens (req.user, /v1/me)
AUTH_JWKS_URL (unset) OIDC JWKS endpoint (RS/ES) — alternative to AUTH_JWT_SECRET
AUTH_JWT_ISSUER / AUTH_JWT_AUDIENCE (unset) Required iss / aud enforced on tokens (recommended)
LOG_LEVEL info Log verbosity: tracefatal, or silent

CORS is browser-ready out of the box: every REST method (GET/POST/PUT/PATCH/ DELETE + OPTIONS preflight) is allowed cross-origin, the API's request headers (Authorization, Content-Type, If-Match, X-Project-Id, X-User-Token) are accepted, and its response headers (ETag, X-Request-Id, the rate-limit headers, Content-Disposition) are exposed so the SDK can read them. CORS_ORIGIN only restricts which origins may call the instance.

Data store — SQLite (default) or Postgres

Data lives in a single SQLite file under DATA_DIR out of the box. For durable or multi-instance deploys, point it at Postgres — the schema is created automatically on first boot, and the API + SDK are byte-for-byte identical.

Variable Default Purpose
DB_DRIVER sqlite sqlite (file under DATA_DIR) or postgres
DATABASE_URL (unset) Postgres connection string when DB_DRIVER=postgres
PG* (unset) Standard libpq vars (PGHOST, PGPORT, PGUSER, PGPASSWORD, PGDATABASE) — an alternative to DATABASE_URL
# Postgres (or set the PG* vars instead of DATABASE_URL)
DB_DRIVER=postgres  DATABASE_URL=postgres://user:pass@host:5432/app

File storage — local disk (default) or S3

Uploads land on local disk under DATA_DIR. For durable or multi-instance deploys, point file storage at S3 or any S3-compatible store. Credentials always come from the standard AWS chain (env, shared profile, or instance/IRSA role) — never config.

Variable Default Purpose
STORAGE_DRIVER disk disk (under DATA_DIR) or s3
S3_BUCKET (unset) Bucket (required when STORAGE_DRIVER=s3)
S3_REGION (unset) Region (falls back to AWS_REGION)
S3_ENDPOINT (unset) Custom endpoint for S3-compatible stores (MinIO/LocalStack)
S3_PREFIX (unset) Optional key namespace (one bucket → several instances)
S3_FORCE_PATH_STYLE false Path-style addressing (MinIO/LocalStack usually need true)
# AWS S3
STORAGE_DRIVER=s3  S3_BUCKET=my-bucket  S3_REGION=us-east-1

# MinIO / LocalStack (S3-compatible)
STORAGE_DRIVER=s3  S3_BUCKET=dev  S3_ENDPOINT=http://localhost:9000  S3_FORCE_PATH_STYLE=true

Realtime — in-memory (default) or Redis (scale-out)

Websocket pub/sub and presence run in-process by default. Set REDIS_URL and run multiple server processes to fan messages + presence across them — horizontal scale with an identical API/SDK.

Variable Default Purpose
REDIS_URL (unset) Redis connection string; enables cross-process realtime fan-out, cluster-wide presence (entries auto-expire if a process dies), and a shared per-IP rate-limit window
REDIS_URL=redis://localhost:6379   # then run N server processes behind a load balancer

AI provider — Anthropic (default), OpenAI-compatible, or Bedrock

/v1/ai/chat speaks one wire format regardless of backend; pick it with AI_PROVIDER.

Variable Default Purpose
AI_PROVIDER anthropic anthropic, openai, or bedrock
AI_MODEL (provider default) Default model id (Claude for anthropic; required for openai/bedrock)
AI_MAX_TOKENS 4096 Default output cap (per-request override allowed)
ANTHROPIC_API_KEY (unset) Key for provider=anthropic
AI_BASE_URL / AI_API_KEY (unset) OpenAI-compatible endpoint + key for provider=openai
AI_HEADERS (unset) Extra request headers (JSON) for provider=openai — non-Bearer auth / gateway headers
AWS_REGION (unset) Region for provider=bedrock (creds via the standard AWS chain)
# Anthropic (default)
ANTHROPIC_API_KEY=sk-ant-...

# Any OpenAI-compatible endpoint — OpenAI, Ollama, vLLM, and LiteLLM:
AI_PROVIDER=openai  AI_BASE_URL=http://localhost:4000  AI_API_KEY=sk-...  AI_MODEL=gpt-4o
#   ^ point AI_BASE_URL at your LiteLLM proxy to use any model it fronts (incl. Bedrock)
# Behind a corporate gateway with non-Bearer auth? Add extra headers as JSON
# (custom keys override the default Authorization):
#   AI_HEADERS='{"api-key":"…","x-org-id":"acme"}'

# Amazon Bedrock directly (Converse API; creds from env/profile/instance role):
AI_PROVIDER=bedrock  AWS_REGION=us-east-1  AI_MODEL=anthropic.claude-3-5-sonnet-20240620-v1:0

GET /v1 reports the active provider; /v1/stats reports the active storage backend.

Limits & operations

Variable Default Purpose
MAX_UPLOAD_BYTES 52428800 (50MB) Per-file / body size cap
RATE_LIMIT_MAX 600 Requests per window per IP (0 disables)
RATE_LIMIT_WINDOW_MS 60000 Rate-limit window length
SHUTDOWN_TIMEOUT_MS 10000 Max drain time on SIGINT/SIGTERM before forced exit

Project layout

src/
  index.ts              # entrypoint: boot + graceful shutdown
  server.ts             # builds the Fastify app, registers every module
  config.ts             # zero-config defaults
  db.ts                 # async DB facade + driver selection (DB_DRIVER)
  db/                   # sqlite (default) + postgres drivers; per-dialect SQL
  lib/id.ts             # id generation
  modules/
    projects/           # ✅ tenants (store.ts) — namespacing for everything below
    admin/              # ✅ project management API (ADMIN_KEY-gated)
    data/               # ✅ document store (store.ts) + REST routes
    acl/                # ✅ per-collection access rules (store.ts)
    files/              # ✅ blob storage (storage.ts) + blob.ts driver (disk/s3) + REST routes
    ai/                 # ✅ chat routes + provider.ts + providers/{anthropic,openai,bedrock}.ts
    backup/             # ✅ whole-instance export/import (API_KEY-gated)
    meta/               # ✅ /v1 discovery, /v1/stats, /admin
    realtime/           # ✅ websocket pub/sub hub (hub.ts) + routes
public/
  sdk.js                # zero-build browser client (served at /sdk.js)
  sdk.d.ts              # TypeScript types for the SDK
  index.html            # demo page built on the SDK
  admin.html            # dashboard (served at /admin)
  llms.txt              # one-read API guide for AI agents
  examples.html         # gallery; examples/*.html — runnable single-file apps
test/
  api.test.ts           # end-to-end suite over HTTP + websockets
  ai-provider.test.ts   # AI provider selection + message normalization
  migration.test.ts     # in-place schema migration
Dockerfile              # multi-stage build → slim runtime image
deploy/
  *.service             # systemd unit for bare-metal installs

Roadmap

Shipped: pluggable backends — optional Postgres (data) and S3 (files) alongside the SQLite + disk defaults; declared indexed fields (built CONCURRENTLY on Postgres) for fast queries on large collections; per-collection JSON Schema validation (opt-in); cursor pagination for stable iteration; per-project public collections + live feeds via a non-secret project id; realtime presence (join/leave/typing) with an optional Redis fan-out, Redis-backed rate limiting, and presence TTLs for multi-process scale; observability — Prometheus /metrics + a /ready probe; outbound webhooks (persisted, retried, HMAC-signed); per-project AI provider config; and verifiable identity (JWT/OIDC) — trusted req.user / /v1/me, trusted presence, and token auth for locked instances.

Next: row-level per-user authorization (own-your-rows ACL) built on the new verified identity; publish to npm + GHCR (the release workflow is wired; the first tag publishes).

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors