openapi.yaml is generated by engine-cli openapi. The API uses PASETO v4 local
access/refresh tokens. Browsers in the full application use a separate encrypted
session gateway; that SvelteKit gateway is not included in this repository.
PASETO_LOCAL_KEY is a 32-byte hex key. WEB_SERVICE_KEY authenticates trusted
service-to-service magic-link issuance and provider identity exchange; do not expose it to browsers.
ADMIN_EMAILS is an explicit allowlist; an empty value denies admin access.
Every environment requires a service key of at least 32 characters and a nonzero PASETO key. TLS termination, provider OAuth/email,
secret management and a browser session gateway are deployment responsibilities.
Run the binary with DATABASE_URL, PASETO_LOCAL_KEY, WEB_SERVICE_KEY, API_HOST,
API_PORT and the intended APP_ENV. It applies bundled migrations on startup.
Binding remains loopback by default. Review the generated API and the permission
checks before exposing the service to a network.
crates/engine-api/tests/api_test.rs and auth_test.rs demonstrate local authenticated
requests, version/draft operations, stale writes and membership isolation against
fresh SQLx test databases. They do not contact real login or research providers.
The synchronized service includes /documents/:id/change-requests (create/list,
detail, merge, decline, withdraw and reviews), draft submit/withdraw,
/documents/:id/history-points and /state?through_seq=, /people, /policy,
/owner, /documents/:id/copies, /copies/:match_id,
/notifications, /notifications/count and /notifications/read.
Use generated OpenAPI for exact verbs, payloads and response shapes.
Per-document rules protect canonical edits and constrain approval/merge rights. Submitted requests are reviewable by permitted collaborators; private drafts stay private. Approvals must reflect current request content. Copy metadata and notices are filtered at read time; a notification does not grant document access.
The API binary starts a background copy-index/detection worker. Fingerprints are
derived data; notifications and review metadata are separate from content events.
Watch/digest APIs are absent. Invitation delivery at application
b70f048 is a web-gateway operation after an engine grant, not an engine endpoint.
See current capabilities for role-name compatibility and limits.
Projects and editor-scoped provenance endpoints are implemented; see the version-1 service contract and generated OpenAPI. Connector keys cannot use account-wide session endpoints or merge operations.
Both provenance checkpoint GET endpoints accept optional through_seq for exact canonical historical state/hash. Current membership and credential checks apply; negative/future revisions return 400. Omitted selector returns the current head. Proposal statuses remain current, including in historical checkpoints.
GET /profile reads the signed-in account profile; POST /profile updates its
name and bio with the expected revision. A stale revision returns 409. GET /profiles/:id returns only the caller or an enabled account currently sharing a
Project with the caller; unavailable profiles return 404. Document-only sharing
is insufficient. Responses omit email. Names are 1-80 Unicode scalar values and
bios at most 280. Account erasure clears this metadata.
Original-file upload accepts at most 3,000,000 decoded bytes and bounds base64
length before decoding. Oversize originals return 400; the transport body limit
can return 413. A rejected upload creates no original-file row. Derived provenance
payloads and expanded-archive safety budgets remain separate. See generated
OpenAPI and profiles_upload_test.rs for exact contracts and boundary checks.
New access tokens and refresh successors share a login ID. POST /auth/logout accepts a refresh_token secret, including an unexpired rotated ancestor, and revokes that login. Protected requests and SSE data delivery check revocation. Logout is idempotent; an unknown secret discloses no account state. Independent logins remain active. A pre-schema-27 cookie lacks a login ID, so its logout invalidates the account generation instead, including existing connector grants. See compatibility and deployment gates.
Event pages are bounded in SQL (up to 1,000 rows plus lookahead). SSE catches up in 100-row pages before live delivery, supports Last-Event-ID and rejects negative or future positions. Byte/tenant/time limits and expensive search remain open.
GET /documents/:id/verify independently checks the event chain and all legacy
snapshots against replay. Snapshot/content/public-link/history/restore readers
require immutable verification receipts. CLI verify-snapshots DOCUMENT_UUID or
verify-snapshots --all performs verified backfill; CLI verify checks independently
without writing receipts. An existing database must complete this backfill before
traffic is restored. An old API that ignores receipts is not safe rollback. Read
the release gate and limitations.
Caller-created NodeCreated.node_id and ChoiceAdded.choice_id must be canonical
uppercase ULIDs: exactly 26 ASCII Crockford base32 characters, with first character
0–7. Node-create, batch, draft edit, change-request creation and proposed
suggestion operations use the same check. Malformed values return HTTP 400 before
any operation is applied. Missing node-create IDs still use server-generated ULIDs.
Sync included/dropped selections and change-request merge selections accept at most 20,000 IDs of 1 to 128 bytes each (history recovery keeps its 5,000-ID limit and the same per-ID bound). A selection names nodes that already exist, so it is not required to be canonical: an old node with a non-canonical ID can still be shared, merged or recovered. Error messages never include supplied IDs.
This is a write-boundary change for new identities only. Historical event readers,
NodeId deserialization, server-generated history recovery operations and references
to existing nodes keep legacy ID compatibility. Existing successful idempotency
responses can still replay. A canonical ULID alone does not reserve a global node ID;
database uniqueness remains authoritative. No historical IDs or migrations change.
Canonical node IDs are globally unique. A node-create, batch, draft share or
new choice that collides with another document's ID returns HTTP 409 with a generic
message containing neither the conflicting ID nor the other document. The entire
transaction rolls back, including earlier operations in a batch. Only a PostgreSQL
23505 violation of node_pkey receives this mapping; other database errors keep
their existing handling.
Change-request/connector uploads and private draft edits check new IDs after authorization and before storing new draft content. A connector bundle's existing idempotent receipt can still replay first. This is early feedback, not an ID reservation: another document can commit the ID later, and sharing still relies on the primary key. Existing historical identities and node references are not rewritten; this builds on the stricter new-ID wire contract.
These are write-boundary rules for new operations. Replay of stored events, snapshots and the materialized fold are unchanged, so existing histories keep their meaning and hashes. Each violation returns HTTP 400 (HTTP 409 for an ID collision) before anything is appended, and a batch that contains one is rolled back as a whole.
NodeRestoredapplies only to a node that is deleted or to a removed choice. Restoring a node that is already live returns 400 ("already live").- A removed choice (after
ChoiceRemoved) accepts noFieldEdited,RichTextPatched,NodeMovedor secondChoiceRemoved; restore it first withNodeRestored. It can still be tombstoned withNodeDeleted, as an editor does when it deletes the whole question. NodeCreated.posandNodeMoved.new_pos(also inside a proposed suggestion) must be 1 to 8,192 bytes of printable ASCII (space to~) and must not end in a space. Every key the web editor'sbetween()and the agent compiler's port of it produce satisfies this; server-generated operations and stored keys are not checked.