API for Luminary, built with Nest and CouchDB.
The following software is needed to run and/or test the Luminary API:
- CouchDB (document database) - see https://couchdb.apache.org
- S3 (compatible) storage, e.g. MinIO - see https://min.io
For development purposes, CouchDB can be installed as a docker:
docker run -p 5984:5984 -d -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=yourpassword couchdbAfter successfully running CouchDB, create a local database via the CouchDB web interface at http://localhost:5984/\_utils/ using the credentials you set above.
For development purposes, MinIO can be installed as a docker for S3 compatible storage:
This command will create an instance with a pre-configured access key / secret combination:
docker run -d -p 9000:9000 -p 9001:9001 --name luminary-storage -e "MINIO_ACCESS_KEY=minio" -e "MINIO_SECRET_KEY=minio123" quay.io/minio/minio server /data --console-address ":9001"docker run -d \
-p 9000:9000 \
-p 9001:9001 \
--name luminary-storage \
-e "MINIO_ACCESS_KEY=minio" \
-e "MINIO_SECRET_KEY=minio123" \
quay.io/minio/minio server /data --console-address ":9001"If you need to log into the MinIO web console, the root user and password can be passed instead. Note that you manually will have to create an access key / secret combination and update your .env file accordingly. The web console is available on http://localhost:9001
docker run -d \
-p 9000:9000 \
-p 9001:9001 \
--name luminary-storage \
-e "MINIO_ROOT_USER=rootuser" \
-e "MINIO_ROOT_PASSWORD=password" \
quay.io/minio/minio server /data --console-address ":9001"- Copy the environment variable file and fill in required fields, such as the database connection string:
cp .env.example .env- Install dependencies:
$ npm ci- Seeding the database:
Before running Luminary against a clean CouchDB database it is recommended to seed the database with the default document set. This document set is also used for unit tests, and should help you to get a functional setup to start with.
$ npm run seedBy default the API will run at http://localhost:3000.
- Run the server:
# development
$ npm run start
# watch mode
$ npm run start:dev # or just 'dev'
# production mode
$ npm run build
$ npm run start:prodCopy and .env.test.example file to .env.test and set the required values, such as the database connection string.
cp .env.test.example .env.testRun the unit tests:
# unit tests
$ npm run test:unit
# test coverage
$ npm run test:covJest (included with NestJS) is used for automated API unit testing. Each test file gets a fresh, seeded CouchDB database via createTestingModule (see src/test/testingModule.ts), seeded from src/db/designDocs and src/db/seedingDocs.
The Jest unit testing currently does not include teardown logic. Destroying documents does not delete them from the CouchDB database, it marks them as deleted — this does not work well with testing where we reuse the testing database (i.e. local testing on your computer), since CouchDB / nano complains that the document is deleted and hence cannot be recreated. In real-life scenarios recreating a deleted document should never occur, and if it does this would be an exception that should be logged to an error log instead of being processed. It therefore does not work to destroy our testing data set for local testing — the developer can rather delete the testing database on their computer once in a while if it gets too big.
# lint code and output errors
$ npm run lint
# lint code and fix auto-fixable errors
$ npm run lint:fixIn production mode (npm run start:prod) the API logs are stored in a tailable api.log file. The log files are rotated when the size exceeds 1MB and only the latest 5 files are being kept. In development mode logs are printed to the console.
The load tester currently tests the API for Luminary Client app sync loads on the /query API endpoint.
# load tester help
$ npx ts-node load_tester --help- docs/rest-api/README.md — REST bulk sync API and SyncMap
- docs/socket-io-messages.md — Socket.io message reference (API ↔ clients)
- docs/s3-multi-bucket/README.md — S3 multi-bucket storage architecture
The CMS sees more than the app — drafts, scheduled, and expired Content — and that extra
visibility is gated by a dedicated ACL permission, CmsView (GitHub #160, see ADR 0013).
Plain View is the app/public gate (published content only); CmsView is the CMS gate.
Every read carries a cms flag: the CMS sends cms: true, the app cms: false. The flag
requests CMS-scoped results; the actual gate is the permission:
POST /queryandPOST /ftsselect the permission per request:cms ? CmsView : View. Acms: truerequest returns drafts/expired only for groups where the caller holdsCmsView. If the caller holds noCmsViewon any requested group, the request is 403 Forbidden (the same fail-closed behaviour as a missingView) — it does not silently fall back to published-only.QueryServiceremains the data-leakage boundary; for non-cmsrequests it still injects the published/scheduled/expiry filters before the query runs.
Live updates can't read the cms flag off each message, and the AccessMap is per-user (a CMS
user holds both View and CmsView), so the connection declares its mode in the clientConfigReq
handshake (cms: true | false; the API also accepts the deprecated joinSocketGroups alias for
older clients, see ADR 0005). The server routes it to one of two room sets per group:
- base
${docType}-${group}— app connections join these viaView. ${docType}-${group}-cms— CMS connections join these viaCmsView.
A CMS connection joins only -cms rooms (never base), so base-room guarding can never corrupt
its full copy. deleteCmd-${group} rooms are shared (un-suffixed) by both modes. On a document
update:
-cmsrooms receive the full doc for every status (published, draft, expired) — this is what gives CMS users live draft/expired collaboration.- base rooms receive only what the app may hold: published-and-live Content and all non-Content docs in full; expired Content as a stripped cleanup stub (see below); draft Content is withheld entirely.
A non-CMS client only ever receives an expired Content doc so it can prune its stale local copy
(it never displays it), so the body must not cross the wire. api/src/util/stripExpiredContent.ts
projects an expired doc down to a minimal cleanup stub (identity, grouping, status/expiry, sync
cursor — no title/body/SEO/media/FTS). The same projection is applied in two places: the /query
response for non-cms callers, and the Socket.io base-room emit.
The stub still carries expiryDate/status, so the app's deleteExpired() prunes the doc rather
than orphaning the stale, still-visible version on the device (the edge case from #433). Do not
"optimize" the base-room rule to drop expired Content entirely — that reintroduces the orphan bug.
Unpublish (published → draft) is handled separately: the now-draft doc is withheld from base rooms
and the app is evicted by the existing app-only DeleteReason.StatusChange DeleteCmd.
CMS users are auto-granted CmsView on deploy by schema upgrade v19 (every ACL entry that already
has Edit/Translate/Publish), so existing editors keep working through the rollout.