Skip to content

feat: add custom fields API support#359

Merged
taqh merged 4 commits into
mainfrom
feat/api-custom-fields
Jun 19, 2026
Merged

feat: add custom fields API support#359
taqh merged 4 commits into
mainfrom
feat/api-custom-fields

Conversation

@taqh

@taqh taqh commented Jun 18, 2026

Copy link
Copy Markdown
Member

Description

Adds first-class custom field support to the public API and MCP server. This includes /v1/fields read/write endpoints, OpenAPI schemas for field definitions and options, key-based custom field validation for post create/update, MCP field tools, and docs updates for the API/SDK/MCP workflow.

Motivation and Context

Custom fields already existed in the data model and were returned on post reads, but field definitions and post field writes were not exposed through the public API or MCP server. This lets agents and SDK/API users deliberately create field definitions, validate post field payloads against existing schema, and write custom field values without implicit schema creation.

How to Test

  1. Run pnpm lint.
  2. Run pnpm exec tsc -p apps/mcp/tsconfig.json --noEmit.
  3. Start the API locally with Wrangler.
  4. Verify public keys can read fields but cannot write them.
  5. Verify invalid field definitions return structured 400 responses.
  6. Create select, multiselect, and required text fields with a private key.
  7. Verify post creation rejects unknown field keys, invalid select values, invalid multiselect values, and missing required fields.
  8. Verify valid post creation stores custom field values and post readback returns typed values.
  9. Verify post update can clear optional field values with null.
  10. Verify changing a field type/options is blocked after values exist.

Validation performed locally:

  • pnpm lint
  • pnpm exec tsc -p apps/mcp/tsconfig.json --noEmit
  • Local Wrangler API validation suite covering field permissions, bad payloads, valid post writes, typed readback, update validation, unsafe schema changes, and cleanup
  • Local smoke test confirming field validation errors return { error, message, details }

Screenshots (if applicable)

N/A

Video Demo (if applicable)

N/A

Types of Changes

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • ⚠️ Breaking change (fix or feature that alters existing functionality)
  • 🎨 UI/UX Improvements
  • ⚡ Performance Enhancement
  • 📖 Documentation (updates to README, docs, or comments)

Summary by CodeRabbit

New Features

  • Added workspace-scoped custom fields API with full CRUD for field definitions, including select/multiselect option support and structured validation errors.
  • Posts now accept custom field values on create/update, persisting them transactionally.
  • Added MCP tools for listing, fetching, creating, updating, and deleting custom fields.

Documentation

  • Expanded custom fields docs with API/SDK/MCP workflows and JSON examples for defining fields and writing values.

Bug Fixes

  • Improved cache invalidation to include custom fields (so updates reflect correctly).

@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
marble-app Ready Ready Preview, Comment Jun 19, 2026 7:38pm
marble-web Ready Ready Preview, Comment Jun 19, 2026 7:38pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4afea2d9-cbfa-481b-ab48-124418fdc9ec

📥 Commits

Reviewing files that changed from the base of the PR and between 9f08095 and b075656.

📒 Files selected for processing (2)
  • apps/api/src/routes/fields.ts
  • packages/db/src/workers.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/api/src/routes/fields.ts

Walkthrough

Adds a complete custom fields system: Zod/OpenAPI schemas for field types and options, a fields CRUD route with unsafe-change guards and cache invalidation, a validation/resolution library (validateFieldValue, resolveCustomFieldValuesByKey), transactional custom-field value persistence in post create/update flows, MCP tool registration, and documentation updates.

Changes

Custom Fields Feature

Layer / File(s) Summary
Field schemas and field option input validation
apps/api/src/schemas/fields.ts
Defines Zod/OpenAPI schemas for field types, field options, output envelopes, and CreateFieldBodySchema/UpdateFieldBodySchema with superRefine validation for option-presence and uniqueness rules.
Custom field write schemas for post bodies
apps/api/src/schemas/posts.ts
Introduces CustomFieldsWriteSchema (record of field key to string/number/boolean/string-array/null values) and integrates it as an optional fields property in post create and update request bodies.
Custom field value validation and resolution library
apps/api/src/lib/fields.ts
Implements validateFieldValue (type-specific normalization, required/empty handling, ISO date, multiselect deduplication, richtext sanitization) and resolveCustomFieldValuesByKey (create vs update mode, unknown key rejection, returns FieldValueWrite[] or CustomFieldValidationError).
Cache invalidation support for fields resource
apps/api/src/lib/cache.ts
Extends invalidateResource method to accept "fields" as a valid resource type for cache key targeting.
Fields CRUD route handlers and OpenAPI contracts
apps/api/src/routes/fields.ts
Adds list, get, create, update, and delete field handlers with option-safety checks, unsafe type/option-change guards that reject modifications when values exist, DB transaction for atomic option replacement, conflict detection (409), and cache invalidation for fields and posts.
App routing wiring for fields endpoints
apps/api/src/app.ts
Mounts fieldsRoutes on both the legacy workspace-scoped router (/:workspaceId/fields) and API-key router (/fields/v1/fields).
Post create/update integration with custom field persistence
apps/api/src/routes/posts.ts
Extends post creation to resolve field definitions and wrap post insert and fieldValue inserts in a single DB transaction; extends post update to resolve custom-field writes in update mode and apply upsert/deleteMany inside a transaction.
MCP fields tools registration and post tools extension
apps/mcp/src/tools/fields.ts, apps/mcp/src/server.ts, apps/mcp/src/tools/posts.ts
Adds registerFieldTools with five MCP tools wired to /v1/fields endpoints, registers it in createServer, and extends create_post/update_post MCP schemas with an optional fields property.
Database Prisma export for workers
packages/db/src/workers.ts
Exports the Prisma type namespace alongside createClient for worker contexts.
Documentation updates
apps/docs/features/custom-fields.mdx, apps/docs/tools/mcp.mdx, apps/docs/tools/sdk.mdx
Adds API usage guidance with example payloads, 400 rejection conditions, no-implicit-create behavior note, and Fields sections in MCP and SDK tool reference pages.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant PostsRouter
  participant resolveCustomFieldValuesByKey
  participant FieldsRouter
  participant DB
  participant Cache

  Note over Client,Cache: Custom Field Definition (one-time setup)
  Client->>FieldsRouter: POST /v1/fields { key, type, options? }
  FieldsRouter->>DB: check key conflict, create field + options
  FieldsRouter->>Cache: invalidate "fields" and "posts"
  FieldsRouter-->>Client: 201 { field }

  Note over Client,Cache: Writing field values on a post
  Client->>PostsRouter: POST /v1/posts { title, fields: { my_key: "value" } }
  PostsRouter->>DB: fetch field definitions for workspace
  PostsRouter->>resolveCustomFieldValuesByKey: resolve in "create" mode
  resolveCustomFieldValuesByKey-->>PostsRouter: FieldValueWrite[] or 400 error
  PostsRouter->>DB: $transaction create post + insert fieldValue rows
  PostsRouter->>Cache: invalidate "posts"
  PostsRouter-->>Client: 201 { post }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • usemarble/marble#311: Both PRs modify the posts API's create/update logic in apps/api/src/routes/posts.ts; the retrieved PR adds full POST/PATCH CRUD with content sanitization, while the main PR extends the same POST/PATCH flows to also resolve and persist custom fields via transactional field-value persistence.
  • usemarble/marble#356: Main PR adds the /v1/fields workspace-scoped and API-key routes and expands cache invalidation to include the "fields" resource; the retrieved PR introduces API-key scope authorization that derives required scopes from path resources (including the new "fields" mapping), directly protecting these new endpoints.

Poem

🐇 A field of fields, now properly sown,
With keys and types and options well-known.
A transaction wraps each value with care,
Safe updates guarded against unsafe snare.
The rabbit hops happy—custom fields are here! 🌱

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding custom fields API support, which aligns with the comprehensive feature implementation across multiple files.
Description check ✅ Passed The PR description covers all required template sections with substantive content: description, motivation/context, testing steps, and marked type of change. All key details are provided.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/api-custom-fields

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 18, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
marble-mcp b075656 Commit Preview URL

Branch Preview URL
Jun 19 2026, 07:38 PM

@mintlify

mintlify Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
marblecms 🟢 Ready View Preview Jun 18, 2026, 11:23 PM

@taqh taqh changed the title [codex] Add custom fields API support feat: add custom fields API support Jun 18, 2026
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 18, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
marble-api b075656 Commit Preview URL

Branch Preview URL
Jun 19 2026, 07:39 PM

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/lib/fields.ts`:
- Around line 194-201: The validation logic is checking if the value is empty
before sanitizing the HTML, but required rich-text fields should be validated
after sanitization since HTML sanitization can result in an empty string even if
the original input was non-empty. Reorder the logic to first sanitize the value
using sanitizeHtml, then check if the sanitized result is empty using
isRichTextContentEmpty, and only then return an error if the field is required
and the sanitized value is empty. This prevents required rich-text fields from
being stored as empty after sanitization.

In `@apps/api/src/schemas/fields.ts`:
- Around line 134-138: The name field validation in the Zod schema currently
allows whitespace-only strings because they pass the `.min(1)` check despite
being functionally empty. Add a `.trim()` method call before the `.min(1, "Name
cannot be empty")` validation in the name field schema to strip leading and
trailing whitespace, ensuring that whitespace-only names are properly rejected.
This same fix also needs to be applied to the similar name validation block
mentioned at lines 172-177.

In `@apps/docs/features/custom-fields.mdx`:
- Around line 55-90: The API documentation section "Writing fields through the
API" contains only raw JSON payload snippets without showing the complete
request/response workflow. Replace the first JSON code block (starting with the
field definition containing "key": "audience") with a RequestExample component
that includes the HTTP method (POST), endpoint path (/v1/fields), a curl command
with proper Authorization and Content-Type headers, and the JSON payload. Add
corresponding ResponseExample components for success (status 201), validation
errors (status 400), and rate limiting (status 429). Apply the same pattern to
the second JSON snippet (for creating/updating posts) by wrapping it in
RequestExample/ResponseExample components showing the appropriate endpoint,
method, headers, and response cases. Ensure all examples are executable curl
commands that readers can copy and test directly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 70e9b658-c075-423a-81df-90513209285d

📥 Commits

Reviewing files that changed from the base of the PR and between edc9506 and bae26a4.

📒 Files selected for processing (13)
  • apps/api/src/app.ts
  • apps/api/src/lib/cache.ts
  • apps/api/src/lib/fields.ts
  • apps/api/src/routes/fields.ts
  • apps/api/src/routes/posts.ts
  • apps/api/src/schemas/fields.ts
  • apps/api/src/schemas/posts.ts
  • apps/docs/features/custom-fields.mdx
  • apps/docs/tools/mcp.mdx
  • apps/docs/tools/sdk.mdx
  • apps/mcp/src/server.ts
  • apps/mcp/src/tools/fields.ts
  • apps/mcp/src/tools/posts.ts

Comment thread apps/api/src/lib/fields.ts Outdated
Comment thread apps/api/src/schemas/fields.ts
Comment on lines +55 to +90
## Writing fields through the API

Create field definitions first:

```json
{
"key": "audience",
"name": "Audience",
"type": "multiselect",
"required": false,
"options": [
{ "value": "developers", "label": "Developers" },
{ "value": "founders", "label": "Founders" }
]
}
```

Then pass values by field key when creating or updating posts:

```json
{
"title": "Launch Notes",
"content": "<p>Hello world</p>",
"description": "What changed this week",
"slug": "launch-notes",
"categoryId": "cat_123",
"status": "draft",
"fields": {
"audience": ["developers", "founders"]
}
}
```

Unknown field keys, wrong value types, and option values that do not match a
configured `select` or `multiselect` option are rejected with `400` responses.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace payload-only JSON snippets with full API request/response examples.

This API section currently shows body fragments only, so readers can’t execute or verify the workflow end-to-end (auth, endpoint, status, success/error responses, rate-limit behavior).

Suggested docs structure update
 ## Writing fields through the API

-Create field definitions first:
-
-```json
-{
-  "key": "audience",
-  "name": "Audience",
-  "type": "multiselect",
-  "required": false,
-  "options": [
-    { "value": "developers", "label": "Developers" },
-    { "value": "founders", "label": "Founders" }
-  ]
-}
-```
+<RequestExample method="POST" path="/v1/fields">
+```bash title="create-field.sh"
+curl -X POST "https://api.marblecms.com/v1/fields" \
+  -H "Authorization: Bearer mb_private_xxx" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "key": "audience",
+    "name": "Audience",
+    "type": "multiselect",
+    "required": false,
+    "options": [
+      { "value": "developers", "label": "Developers" },
+      { "value": "founders", "label": "Founders" }
+    ]
+  }'
+```
+</RequestExample>
+
+<ResponseExample status={201}>
+```json
+{ "field": { "key": "audience", "type": "multiselect", "required": false } }
+```
+</ResponseExample>
+
+<ResponseExample status={400}>
+```json
+{ "error": "Invalid custom field value", "message": "..." }
+```
+</ResponseExample>
+
+<ResponseExample status={429}>
+```json
+{ "error": "Rate limit exceeded", "message": "Limit: 60 requests/minute" }
+```
+</ResponseExample>
As per coding guidelines, “Always include complete, runnable code examples that users can copy and execute”, “Cover complete request/response cycles in API documentation”, “Show both success and error response examples with realistic data in API documentation”, “Include rate limiting information with specific limits in API documentation”, “Provide authentication examples showing proper format in API documentation”, and “Use RequestExample/ResponseExample components specifically for API endpoint documentation.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/docs/features/custom-fields.mdx` around lines 55 - 90, The API
documentation section "Writing fields through the API" contains only raw JSON
payload snippets without showing the complete request/response workflow. Replace
the first JSON code block (starting with the field definition containing "key":
"audience") with a RequestExample component that includes the HTTP method
(POST), endpoint path (/v1/fields), a curl command with proper Authorization and
Content-Type headers, and the JSON payload. Add corresponding ResponseExample
components for success (status 201), validation errors (status 400), and rate
limiting (status 429). Apply the same pattern to the second JSON snippet (for
creating/updating posts) by wrapping it in RequestExample/ResponseExample
components showing the appropriate endpoint, method, headers, and response
cases. Ensure all examples are executable curl commands that readers can copy
and test directly.

Source: Coding guidelines

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a24733f720

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/api/src/routes/fields.ts Outdated
Comment on lines +464 to +466
const field = await db.$transaction(async (tx) => {
if (typeChanged || optionsChanged) {
const fieldValueCount = await tx.fieldValue.count({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use serializable isolation for schema-change checks

When the API changes a field's type or options, this transaction counts existing fieldValue rows and then updates the field under the default isolation level. If a post create/update writes a value for the same field concurrently after the count returns 0, the schema update can still commit, leaving saved values that were validated against the old type/options. Use the same serializable transaction/locking approach as the dashboard field update path so the “no saved values before schema changes” invariant holds under concurrent API traffic.

Useful? React with 👍 / 👎.

Comment thread apps/api/src/lib/fields.ts Outdated
.filter((field) => field !== undefined);

for (const field of fieldsToValidate) {
const validation = validateFieldValue(field, json[field.key]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use own-property checks for custom field lookup

For a workspace field whose key is allowed by the create schema but exists on Object.prototype (for example constructor or __proto__), omitting that key during post creation reads the inherited value from {} instead of undefined. In create mode that makes optional fields fail type validation even though the client did not provide them, so look up values only when the parsed fields object has the key as an own property.

Useful? React with 👍 / 👎.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 19, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
marble-jobs b075656 Commit Preview URL

Branch Preview URL
Jun 19 2026, 07:38 PM

@taqh taqh merged commit b6d07d2 into main Jun 19, 2026
15 checks passed
@taqh taqh deleted the feat/api-custom-fields branch June 19, 2026 19:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant