@@ -29,7 +29,8 @@ GET /.well-known/objectstack HTTP/1.1
2929Host: api.acme.com
3030```
3131
32- ` /.well-known/objectstack ` redirects to the canonical discovery route:
32+ ` /.well-known/objectstack ` and the versioned ` /api/v1/discovery ` route both return the
33+ discovery document directly — there is no HTTP redirect between them:
3334``` http
3435GET /api/v1/discovery HTTP/1.1
3536```
@@ -49,16 +50,12 @@ GET /api/v1/discovery HTTP/1.1
4950 "storage" : " /api/v1/storage" ,
5051 "graphql" : " /api/v1/graphql"
5152 },
52- "features" : {
53- "graphql" : true ,
54- "websockets" : false ,
55- "search" : true ,
56- "files" : true ,
57- "analytics" : true ,
58- "ai" : false ,
59- "workflow" : true ,
60- "notifications" : true ,
61- "i18n" : true
53+ "services" : {
54+ "data" : { "enabled" : true , "status" : " available" , "route" : " /api/v1/data" , "provider" : " objectql" },
55+ "metadata" : { "enabled" : true , "status" : " available" , "route" : " /api/v1/meta" , "provider" : " objectql" },
56+ "auth" : { "enabled" : true , "status" : " available" , "route" : " /api/v1/auth" , "provider" : " plugin-auth" },
57+ "workflow" : { "enabled" : false , "status" : " unavailable" , "message" : " Install plugin-workflow to enable" },
58+ "ai" : { "enabled" : false , "status" : " unavailable" , "message" : " Install plugin-ai to enable" }
6259 },
6360 "locale" : {
6461 "default" : " en-US" ,
@@ -71,7 +68,7 @@ GET /api/v1/discovery HTTP/1.1
7168** Why discovery matters:**
7269- ** Environment agnostic:** Works across dev, staging, production without hardcoding URLs
7370- ** Version tolerance:** API routes can change without breaking clients
74- - ** Feature detection:** Clients enable/disable features based on server capabilities
71+ - ** Feature detection:** Clients enable/disable features by inspecting each entry's ` enabled ` / ` status ` in the ` services ` map
7572- ** Automatic configuration:** SDKs auto-configure from discovery response
7673
7774## Standard Data API
@@ -147,32 +144,29 @@ Authorization: Bearer <token>
147144
148145** Success Response:**
149146
150- List responses are wrapped in the standard ` { success, data } ` envelope. For
151- queries, ` data ` is a ` FindDataResponse ` containing ` object ` , ` records ` , and the
152- optional ` total ` / ` hasMore ` pagination hints.
147+ A list query returns a ` FindDataResponse ` directly — there is no outer envelope.
148+ The response carries the ` object ` name, the ` records ` array , and the optional
149+ ` total ` / ` hasMore ` pagination hints.
153150
154151``` json
155152{
156- "success" : true ,
157- "data" : {
158- "object" : " task" ,
159- "records" : [
160- {
161- "id" : " task_456" ,
162- "title" : " Implement login page" ,
163- "status" : " in_progress" ,
164- "created_at" : " 2024-01-15T10:30:00Z"
165- },
166- {
167- "id" : " task_789" ,
168- "title" : " Fix navigation bug" ,
169- "status" : " todo" ,
170- "created_at" : " 2024-01-14T16:20:00Z"
171- }
172- ],
173- "total" : 47 ,
174- "hasMore" : true
175- }
153+ "object" : " task" ,
154+ "records" : [
155+ {
156+ "id" : " task_456" ,
157+ "title" : " Implement login page" ,
158+ "status" : " in_progress" ,
159+ "created_at" : " 2024-01-15T10:30:00Z"
160+ },
161+ {
162+ "id" : " task_789" ,
163+ "title" : " Fix navigation bug" ,
164+ "status" : " todo" ,
165+ "created_at" : " 2024-01-14T16:20:00Z"
166+ }
167+ ],
168+ "total" : 47 ,
169+ "hasMore" : true
176170}
177171```
178172
@@ -286,13 +280,10 @@ record count and a `hasMore` flag:
286280
287281``` json
288282{
289- "success" : true ,
290- "data" : {
291- "object" : " account" ,
292- "records" : [],
293- "total" : 247 ,
294- "hasMore" : true
295- }
283+ "object" : " account" ,
284+ "records" : [],
285+ "total" : 247 ,
286+ "hasMore" : true
296287}
297288```
298289
@@ -311,18 +302,15 @@ GET /api/data/account?select=id,name,industry,revenue
311302** Response:**
312303``` json
313304{
314- "success" : true ,
315- "data" : {
316- "object" : " account" ,
317- "records" : [
318- {
319- "id" : " acc_123" ,
320- "name" : " Acme Corp" ,
321- "industry" : " Technology" ,
322- "revenue" : 5000000
323- }
324- ]
325- }
305+ "object" : " account" ,
306+ "records" : [
307+ {
308+ "id" : " acc_123" ,
309+ "name" : " Acme Corp" ,
310+ "industry" : " Technology" ,
311+ "revenue" : 5000000
312+ }
313+ ]
326314}
327315```
328316
@@ -345,28 +333,25 @@ GET /api/data/task?expand=assignee,project
345333** Response:**
346334``` json
347335{
348- "success" : true ,
349- "data" : {
350- "object" : " task" ,
351- "records" : [
352- {
353- "id" : " task_123" ,
354- "title" : " Implement API" ,
355- "assignee_id" : " user_456" ,
356- "project_id" : " proj_789" ,
357- "assignee" : {
358- "id" : " user_456" ,
359- "name" : " John Doe" ,
360- "email" : " john@acme.com"
361- },
362- "project" : {
363- "id" : " proj_789" ,
364- "name" : " CRM Rebuild" ,
365- "status" : " active"
366- }
336+ "object" : " task" ,
337+ "records" : [
338+ {
339+ "id" : " task_123" ,
340+ "title" : " Implement API" ,
341+ "assignee_id" : " user_456" ,
342+ "project_id" : " proj_789" ,
343+ "assignee" : {
344+ "id" : " user_456" ,
345+ "name" : " John Doe" ,
346+ "email" : " john@acme.com"
347+ },
348+ "project" : {
349+ "id" : " proj_789" ,
350+ "name" : " CRM Rebuild" ,
351+ "status" : " active"
367352 }
368- ]
369- }
353+ }
354+ ]
370355}
371356```
372357
@@ -394,10 +379,15 @@ Authorization: Bearer <token>
394379```
395380
396381** Success Response (HTTP 200):**
382+
383+ A single-record read returns a ` GetDataResponse ` : the ` object ` name, the record
384+ ` id ` , and the full record under ` record ` .
385+
397386``` json
398387{
399- "success" : true ,
400- "data" : {
388+ "object" : " account" ,
389+ "id" : " acc_123" ,
390+ "record" : {
401391 "id" : " acc_123" ,
402392 "name" : " Acme Corporation" ,
403393 "industry" : " Technology" ,
@@ -446,10 +436,15 @@ Content-Type: application/json
446436```
447437
448438** Success Response (HTTP 201):**
439+
440+ Create returns a ` CreateDataResponse ` : the ` object ` name, the new record ` id ` , and
441+ the created record (including server-generated fields) under ` record ` .
442+
449443``` json
450444{
451- "success" : true ,
452- "data" : {
445+ "object" : " account" ,
446+ "id" : " acc_124" ,
447+ "record" : {
453448 "id" : " acc_124" ,
454449 "name" : " TechStart Inc" ,
455450 "industry" : " SaaS" ,
@@ -505,10 +500,15 @@ Content-Type: application/json
505500```
506501
507502** Success Response (HTTP 200):**
503+
504+ Update returns an ` UpdateDataResponse ` : the ` object ` name, the record ` id ` , and the
505+ updated record under ` record ` .
506+
508507``` json
509508{
510- "success" : true ,
511- "data" : {
509+ "object" : " account" ,
510+ "id" : " acc_123" ,
511+ "record" : {
512512 "id" : " acc_123" ,
513513 "name" : " Acme Corporation" ,
514514 "industry" : " Technology" ,
@@ -524,20 +524,14 @@ Content-Type: application/json
524524** Note:** Only fields included in the request body are updated. Other fields remain unchanged.
525525
526526** Read-only fields:**
527- Attempting to update read-only fields (e.g., ` id ` , ` created_at ` ) returns HTTP 400:
527+ Caller-supplied writes to statically read-only fields (e.g., ` id ` , ` created_at ` ) are
528+ ** silently stripped** from a non-system update rather than rejected (#2948 ): the
529+ request succeeds with HTTP 200 and every other field is applied, but the read-only
530+ field is left unchanged.
528531
529- ``` json
530- {
531- "error" : " Cannot update read-only fields" ,
532- "code" : " VALIDATION_FAILED" ,
533- "fields" : [
534- {
535- "field" : " created_at" ,
536- "message" : " Field is read-only"
537- }
538- ]
539- }
540- ```
532+ > ** Note:** This differs from field-level security. A write to a field the caller
533+ > lacks edit permission on is * rejected* with ` 403 PermissionDeniedError ` , not
534+ > stripped.
541535
542536### Delete Record
543537
@@ -555,31 +549,25 @@ Authorization: Bearer <token>
555549```
556550
557551** Success Response (HTTP 200):**
558- ``` json
559- {
560- "success" : true ,
561- "data" : {
562- "id" : " acc_123" ,
563- "deleted" : true
564- }
565- }
566- ```
567552
568- ** Soft Delete (if enabled): **
569- If object has ` enable.trash ` enabled, record is marked deleted but not removed:
553+ Delete returns a ` DeleteDataResponse ` : the ` object ` name, the record ` id ` , and a
554+ ` success ` flag.
570555
571556``` json
572557{
573- "success" : true ,
574- "data" : {
575- "id" : " acc_123" ,
576- "deleted" : true ,
577- "deleted_at" : " 2024-01-16T12:00:00Z" ,
578- "deleted_by" : " user_456"
579- }
558+ "object" : " account" ,
559+ "id" : " acc_123" ,
560+ "success" : true
580561}
581562```
582563
564+ <Callout type = " warn" >
565+ ` enable.trash ` exists as an object flag, but there is currently ** no soft-delete /
566+ recycle-bin runtime** (ADR-0049): ` DELETE ` performs a hard delete and returns the
567+ ` { object, id, success } ` shape above. Records are removed permanently — there are no
568+ ` deleted_at ` / ` deleted_by ` fields and no restore semantics.
569+ </Callout >
570+
583571** Constraint Violations:**
584572Database constraint failures are surfaced as structured errors. For example, a
585573unique-constraint violation returns HTTP 409:
@@ -677,10 +665,14 @@ lists items of that type. Types are singular (`object`, `view`, `app`, …), so
677665objects are listed at ` /api/v1/meta/object ` .
678666
679667** Response:**
668+
669+ A type listing returns ` { type, items } ` — the requested metadata ` type ` plus the
670+ ` items ` array of matching entries.
671+
680672``` json
681673{
682- "success " : true ,
683- "data " : [
674+ "type " : " object " ,
675+ "items " : [
684676 {
685677 "name" : " account" ,
686678 "label" : " Account" ,
@@ -709,10 +701,15 @@ Authorization: Bearer <token>
709701```
710702
711703** Response:**
704+
705+ A single-item read returns ` { type, name, item } ` — the metadata ` type ` , the item
706+ ` name ` , and the full schema under ` item ` .
707+
712708``` json
713709{
714- "success" : true ,
715- "data" : {
710+ "type" : " object" ,
711+ "name" : " account" ,
712+ "item" : {
716713 "name" : " account" ,
717714 "label" : " Account" ,
718715 "plural_label" : " Accounts" ,
@@ -877,8 +874,9 @@ GET /api/data/account?select=id,name,status
877874### Use Expand for Relations
878875❌ ** Bad:** N+1 queries
879876``` javascript
880- const tasks = await fetch (' /api/data/task' );
881- for (const task of tasks .data ) {
877+ const res = await fetch (' /api/data/task' );
878+ const { records } = await res .json ();
879+ for (const task of records) {
882880 task .assignee = await fetch (` /api/data/user/${ task .assignee_id } ` );
883881}
884882```
0 commit comments