API & platform

The API is the product.

One pipeline behind every operation, sync or async, self-describing end to end. Crift isn't a product with an API bolted on; the API is the product, and one uniform execution model stands behind all of it.

One pipeline. Every operation.

Create a record, update a field, link two records, run an action, alter a table, grant access: all the same four-stage pipeline, in one transaction the executor owns. What each stage may do is enforced, not conventional: an out-of-stage write throws at the accessor, so the architecture can't quietly rot.

The pipeline: every operation, one path
100 PreValidation → 200 PreOperation → [ Main SQL op ] → 300 PostOperation → 400 PostOperationAsync
   (no transaction)    (in transaction)                    (in transaction)     (post-commit, own job tx)

Uniformity is the feature.

What you get, building on it.

Sync & async

Sub-second writes and multi-phase jobs, one model.

A synchronous write returns at commit; durable post-commit work enqueues in the same transaction, so a job exists if and only if the write committed. Long operations return 202 and you poll to done: idempotent retries, at most one job per record, an Idempotency-Key honored on replay.

A REST surface that round-trips

GET it, PATCH it back. No reshaping.

Reads inject each lookup's display companions and re-emit the concurrency token; writes strip them. Dirty-field semantics, present-and-null as an explicit clear, snake_case that never omits nulls, so a field's absence and its null stay distinct.

Querying

Filter, page, and project with plain OData.

A practical OData subset with $select / $orderby / $top / $count, server-defined views you address by name, and a multi-entity $batch that pulls a whole subtree in one round-trip. Each spec is independently access-enforced and egress-shaped.

Concurrency & no-leak

Optimistic concurrency, opt-in. 404, never a tell.

Send the row version for a guarded write (412 on conflict, carrying the current token), or omit it for last-writer-wins. A caller without access gets 404, never 403: existence itself isn't disclosed. Both authorization gates fail closed.

Extensibility

Your logic, the platform's guarantees.

New behavior is a handler bound to a (message, entity, stage, order). Pick a stage and inherit its contract: validate at 100, shape the write at 200, cascade and project at 300, do idempotent post-commit work at 400. The platform's own rules run in the same machine as yours.

A self-describing API

A typed client from a generated spec.

GET /api/openapi.json builds a full OpenAPI 3 from your live model; point any generator at it. Metadata discovery and pipeline introspection let a client ask what the model is and exactly which handlers will run. The execution path is inspectable, not a black box.

Integration & MCP

Agents operate it through the same gates you do.

MCP-native: entities exposed as tools, filtered per caller's access tier, with no weaker automation path. Record-scoped chat, a multipart files API, and polymorphic associations, all flowing through the same access enforcement, audit, and error contract as first-party use.

The AI gateway

One governed choke point for every model call.

Rate limiting, response caching, a circuit breaker, per-model-tier concurrency, and cost tracking against a budget, tunable in settings without a redeploy, provider pluggable. Intelligence with a cost ceiling, a failure boundary, and an audit trail.

Reliability & observability

One id traces a call end to end.

A correlation id is minted per request and stamped through logs and audit. One aggregated health endpoint covers the database, the async worker, AI cost tracking, and ingestion. Fail-closed security, dependency-checked deletes, back-off retries: the guarantees that hold under real traffic.

Wire-format conventions

Learn it once; every endpoint behaves the same.

snake_case, nulls always present, lookups carrying __name / __record_number on read and the bare id on write, __row_version__ for concurrency, GUIDs plus a human-readable record_number. No per-entity special cases, because there are no per-entity endpoints: the surface is generated from the model.

By example.

Records that round-trip: GET it, PATCH it back
GET   /api/v1/data/account/{id}
→ 200 {
    "account_id": "9c…",
    "account_id__name": "Globex",
    "__row_version__": "AAAAAAAAB9E"
  }

PATCH /api/v1/data/account/{id}
→ 200  (updated record, new __row_version__)
Async: 202, then poll to completion
POST /api/v1/kg/ingestion/run
→ 202 { "run_id": "…" }

GET  /api/v1/kg/ingestion/{id}/status
→ { "status": "Running" }   // poll to completion
Graph reads: one round-trip, many entities
POST /api/v1/data/$batch
{
  "reads": [
    { "entity": "lode",  "filter": "root_lode_id eq '…'", "top": 200 },
    { "entity": "crest", "filter": "…" }
  ]
}
→ { "results": { "lode": PagedResponse, "crest": PagedResponse } }

One error envelope, precise codes.

Every failure, from any endpoint, returns one shape. code is a stable key you branch on; correlation_id is always present and matches the server trace. Branch on the code, respect the status, and you get correct behavior without parsing prose.

One envelope for every failure
{
  "error": {
    "code": "stable_identifier",
    "message": "human text",
    "field": "attr_or_null",
    "strategy": "strategy_or_null",
    "correlation_id": "guid",
    "details": { }
  }
}
ConditionStatus · code
Bad data state 422 · validation_rule_failed, range_check_violation
Malformed request 400 · invalid_select_attribute, invalid_filter_syntax
Missing / stale concurrency token 428 · missing_concurrency_token · 412 · concurrency_conflict
Delete blocked by dependents 409 · record_delete_blocked (navigable blockers)
AI budget / rate / provider 402 · 429 · 503 (with Retry-After)
No access 404 (no-leak)