Skip to main content

Data Layer REST API Reference

The Data Layer auto-generates REST endpoints for every module model that is marked autoApi: true in its SQL migration. Modules do not write controllers for standard CRUD operations — the platform generates them from the schema.

See the REST API Overview for authentication, error format, and pagination.

note

For layout brevity, the /api/v1 base path prefix is omitted from the endpoint tables below.


Auto-Generated Endpoints

For every autoApi: true model, the following five endpoints are automatically available:

EndpointOperation
GET /data/{module}/{model}List records (filters, sort, paginated)
GET /data/{module}/{model}/:idSingle record (optional relations via ?select)
POST /data/{module}/{model}Create record
PATCH /data/{module}/{model}/:idPartial update
DELETE /data/{module}/{model}/:idSoft delete (sets deleted_at, retains data)

Example — CRM module, contacts model:

GET /api/v1/data/crm/contacts
GET /api/v1/data/crm/contacts/01j9pcont000000000000001
POST /api/v1/data/crm/contacts
PATCH /api/v1/data/crm/contacts/01j9pcont000000000000001
DELETE /api/v1/data/crm/contacts/01j9pcont000000000000001

Query Language

Filter, sort, and select fields using URL query parameters. Syntax: ?{field}={operator}.{value} (PostgREST-style).

Filter Operators

OperatorSyntaxSQL equivalentExample
eq?status=eq.activeWHERE status = 'active'Exact match
neq?status=neq.deletedWHERE status != 'deleted'Not equal
gt?price=gt.100WHERE price > 100Greater than
lt?price=lt.500WHERE price < 500Less than
gte?score=gte.80WHERE score >= 80Greater than or equal
lte?score=lte.100WHERE score <= 100Less than or equal
like?name=like.alice*WHERE name LIKE 'alice%'Pattern, case-sensitive
ilike?name=ilike.alice*WHERE name ILIKE 'alice%'Pattern, case-insensitive
in?status=in.(active,pending)WHERE status IN ('active','pending')One of values
is?deleted_at=is.nullWHERE deleted_at IS NULLIS NULL / IS NOT NULL

SQL injection is impossible. All filter values are parameterized via sqlc. An unknown operator or unknown field name → 400 Bad Request (RFC 9457: validation-error).

OR Conditions

GET /api/v1/data/crm/contacts?or=(status.eq.vip,tier.eq.premium)

Translates to WHERE (status = 'vip' OR tier = 'premium').

Sorting

GET /api/v1/data/crm/contacts?order=createdAt.desc,name.asc

Multiple fields separated by comma. Each field: {field}.{asc|desc}.

Field Selection

GET /api/v1/data/crm/contacts?select=id,name,email,status

Returns only the specified columns. Useful to reduce response size for list views.

Eager Relations (Joins)

GET /api/v1/data/crm/contacts?select=name,company(name,industry)

Loads the company FK relation in the same request. Max depth: 2 levels:

GET /api/v1/data/crm/contacts?select=name,company(name,owner(email))
  • Level 1: company (FK on contacts)
  • Level 2: owner (FK on company)
  • Level 3: ❌ not supported → 400 Bad Request

Pagination

All list endpoints are cursor-paginated:

GET https://api.septemcore.com/v1/data/crm/contacts?limit=20&cursor=eyJpZCI6IjAx...

Response:

{
"data": [
{
"id": "01j9pcont000000000000001",
"name": "Alice Johnson",
"email": "[email protected]",
"status": "active"
}
],
"meta": {
"cursor": "eyJpZCI6IjAx...",
"hasMore": true,
"total": 1247
}
}
ParameterDefaultMax
limit20100
cursoropaque base64

Create Record

POST https://api.septemcore.com/v1/data/crm/contacts
Authorization: Bearer <access_token>
Content-Type: application/json

{
"name": "Alice Johnson",
"email": "[email protected]",
"status": "active",
"tags": ["vip", "enterprise"]
}

Response 201 Created:

{
"id": "01j9pcont000000000000001",
"name": "Alice Johnson",
"email": "[email protected]",
"status": "active",
"tags": ["vip", "enterprise"],
"tenantId": "01j9pten0000000000000001",
"createdAt": "2026-04-22T03:45:00Z",
"updatedAt": "2026-04-22T03:45:00Z",
"deletedAt": null
}

tenantId is always injected by the Gateway from the JWT — module code cannot override it.


Partial Update

PATCH https://api.septemcore.com/v1/data/crm/contacts/01j9pcont000000000000001
Authorization: Bearer <access_token>
Content-Type: application/json

{
"email": "[email protected]",
"status": "inactive"
}

Only provided fields are updated. All other fields remain unchanged. Response: updated record 200 OK.


Soft Delete

DELETE https://api.septemcore.com/v1/data/crm/contacts/01j9pcont000000000000001
Authorization: Bearer <access_token>

Response 204 No Content. The record is not physically removed — deleted_at is set to the current timestamp. Soft-deleted records are excluded from GET /data/{module}/{model} by default.

To query soft-deleted records:

GET /api/v1/data/crm/contacts?deleted_at=is.notnull

Combining Filters

GET https://api.septemcore.com/v1/data/crm/contacts
?status=eq.active
&company=eq.01j9pcomp000000000000001
&createdAt=gte.2026-01-01T00:00:00Z
&order=name.asc
&select=id,name,email,company(name)
&limit=50

Max 10 filters per request (AND conditions). Exceeded → 400 Bad Request.


Security

Access control is enforced at three layers:

LayerEnforcement
API GatewayJWT validation, rate limiting, OpenAPI schema validation
Data LayerPostgreSQL Row-Level Security (RLS) — WHERE tenant_id = $tenantId injected on every query
RBACAuto-generated permissions per model: {module}.{model}.read, {module}.{model}.write, {module}.{model}.delete

RBAC permissions are automatically registered when a model with autoApi: true is created. They appear in the Role Builder UI without any module configuration.

Modules can add custom business-rule validation via lifecycle hooks registered in module.manifest.json:

HookWhenType
{model}.create.beforeBefore create — can reject request or modify dataSynchronous
{model}.create.afterAfter create — side effects (notifications, events)Async (Event Bus)
{model}.update.beforeBefore update — can rejectSynchronous
{model}.update.afterAfter updateAsync (Event Bus)
{model}.delete.beforeBefore soft delete — can rejectSynchronous
{model}.delete.afterAfter soft deleteAsync (Event Bus)

Limits

ParameterLimitNotes
Max records per page (limit)100Default: 20
Max filters per request10AND conditions only
Max select depth (relations)2 levelsDeeper → 400 Bad Request
Models without autoApiNot exposedOpt-in per model
Response body1 MBExceeded → 413 Payload Too Large

Schema Introspection

Discover available models and their schemas:

GET https://api.septemcore.com/v1/data/crm/models
Authorization: Bearer <access_token>

Response:

{
"data": [
{
"name": "contacts",
"fields": [
{ "name": "id", "type": "uuid", "nullable": false, "primaryKey": true },
{ "name": "name", "type": "text", "nullable": false },
{ "name": "email", "type": "text", "nullable": true },
{ "name": "company_id", "type": "uuid", "nullable": true, "foreignKey": "companies.id" },
{ "name": "created_at", "type": "timestamp", "nullable": false }
]
}
]
}

Data Layer reads information_schema from PostgreSQL. Schema cache is refreshed on module install, update, or restart.


Error Reference

Error typeStatusTrigger
problems/validation-error400Unknown field, unknown operator, missing required field
problems/forbidden403Missing {module}.{model}.read or write or delete permission
problems/not-found404Record not found in tenant, or model not found, or module not installed
problems/relations-depth-exceeded400select nesting > 2 levels
problems/filter-limit-exceeded400More than 10 filter conditions in one request
problems/payload-too-large413Response body would exceed 1 MB