Skip to content

feat: Add publicWrite feature for member-created content - #874

Merged
aXenDeveloper merged 1 commit into
canaryfrom
claude/content-public-write
Oct 13, 2026
Merged

aXenDeveloper merged 1 commit into
canaryfrom
claude/content-public-write

Conversation

@aXenDeveloper

Copy link
Copy Markdown
Owner

Description

What?

Implements the publicWrite feature for the Content Engine, enabling signed-in members to create and edit their own content records through public API routes. This includes:

  • Core publicWrite infrastructure: New publicWrite configuration option for content types with server-stamped ownership, moderator permissions, and strict field allowlists
  • Public write routes: Generated POST/PATCH endpoints at /api/{pluginId}/content/{path} for create and edit operations with full conflict/error handling
  • Authorization layer: Member-only access with ownership validation for edits, plus moderator override capabilities
  • Rate limiting: New contentWriteRateLimiterMiddleware to prevent abuse of write operations
  • Frontend example: Complete discussion create/edit pages in the example plugin demonstrating form handling, field errors, version conflicts, and permission issues
  • Documentation: Three comprehensive guides covering setup, API reference, and frontend implementation
  • Test coverage: Unit tests for public write service, routes, and type safety

Why?

This feature enables community-driven content creation while maintaining security and moderation control. Members can contribute discussions, comments, or other content types without admin intervention, with automatic ownership tracking and moderator oversight capabilities. The implementation follows Content Engine patterns for consistency and includes full type safety through the fetcher.

Test Plan

  • Added comprehensive unit tests in public-write-service.test.ts, public-write-routes.test.ts, and define-public-write.test.ts
  • Added type safety tests in public-write.test-d.ts and updated fetcher.test-d.ts
  • Example plugin includes working create/edit pages demonstrating the feature end-to-end
  • Existing content engine tests updated to include new test fixtures

https://claude.ai/code/session_0191AwoSVjnuPQZsoi1wUy9s

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-13T09:38:01.458144Z e244d7b New commits
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

React Doctor found 17 new issues in 13 files · 17 warnings · score 80 / 100 (Needs work) · 0 fixed · vs canary

17 warnings

src/content/define-public-write.ts

  • ⚠️ L63 Array lookup inside a loop js-set-map-lookups

src/content/server/public-write-service.ts

  • ⚠️ L167 await inside a loop async-await-in-loop
  • ⚠️ L317 await inside a loop async-await-in-loop

src/docs/examples/frame-actions.tsx

  • ⚠️ L25 Heading has no content heading-has-content

src/docs/examples/frame-grid.tsx

  • ⚠️ L50 Heading has no content heading-has-content
  • ⚠️ L64 Heading has no content heading-has-content

src/docs/examples/frame.tsx

  • ⚠️ L23 Heading has no content heading-has-content

src/pages/discussion-edit-page.tsx

  • ⚠️ L194 Heading has no content heading-has-content

src/tanstack/notifications/settings-screen.tsx

  • ⚠️ L147 Heading has no content heading-has-content

src/tanstack/settings/overview.tsx

  • ⚠️ L56 Heading has no content heading-has-content

src/views/auth/settings/ai/ai-usage-content.tsx

  • ⚠️ L361 Heading has no content heading-has-content
  • ⚠️ L387 Heading has no content heading-has-content

src/views/auth/settings/overview/overview.tsx

  • ⚠️ L126 Heading has no content heading-has-content

src/views/auth/settings/overview/profile-summary.tsx

  • ⚠️ L38 React function has high control-flow complexity no-high-complexity-react-function
  • ⚠️ L52 Heading has no content heading-has-content

src/views/auth/settings/settings-group.tsx

  • ⚠️ L44 Heading has no content heading-has-content

src/views/notifications/settings/channels-card.tsx

  • ⚠️ L178 Heading has no content heading-has-content

Reviewed by React Doctor for commit cab4e01. See inline comments for fixes.

@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: 0e811e442a

ℹ️ 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".


// Always structured: a public write route is a new contract, so a unique
// conflict answers with the same JSON shape as a version conflict.
return rethrowAsHttpError(error, {

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 Map fallback errors into the public-write contract

When an otherwise valid write throws a generic ContentInputError—for example, a punctuation-only title whose derived slug normalizes to empty—this fallback produces the legacy plain 400 body. Likewise, an editorial content type with delivery redirects can throw ContentDeliverySlugReserved, which becomes a delivery-conflict 409. The generated routes declare ContentPublicWriteRejection for 400 and zodContentConflict for 409, so fetcher clients infer bodies that these responses do not satisfy and parseContentPublicWriteRejection fails. Map these errors into the declared public-write shapes or widen the response schemas.

AGENTS.md reference: AGENTS.md:L40-L40

Useful? React with 👍 / 👎.

Comment on lines +204 to +207
const reachable = new Set(
rows
.filter(row => isContentBaseRowPublic(row))
.map(row => contentIdKey(row.id as ContentId)),

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 Verify localized relation targets through their translations

For a writable relation whose target content type is localized, filtering only with isContentBaseRowPublic accepts a published base row even when it has no published translation in any enabled language. Such a record cannot be returned by the target's localized public API, yet callers can link to it and probe its existence through successful writes, contradicting the public-readability check. Include the localized translation publication and enabled-language predicates, or reject localized relation targets.

Useful? React with 👍 / 👎.

@aXenDeveloper
aXenDeveloper force-pushed the claude/content-public-write branch from 0e811e4 to e244d7b Compare October 13, 2026 09:33

@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: e244d7bba2

ℹ️ 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 on lines +151 to +154
const isContentWriteRequest = (c: Context): boolean =>
(c.req.method === "POST" || c.req.method === "PATCH") &&
c.req.path.includes("/content/") &&
!c.req.path.includes("/admin/");

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 Match the admin module rather than any admin segment

For a valid plugin ID such as admin or @scope/admin, every public write URL contains /admin/ as part of the plugin ID (for example, /api/@scope/admin/content/topics), so this condition exempts its POST and PATCH routes from the new content-write budget. Identify the module segment after the complete plugin ID instead of searching the entire path for /admin/.

Useful? React with 👍 / 👎.

.limit(1);
await authorize(actor, row ?? null);

const full = await model.service(c).findById(id);

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 Authorize the same snapshot returned for editing

When an AdminCP update reassigns or hides a record between the authorization query and this second query, PostgreSQL can return the newer row even though the actor was authorized against the older owner/visibility state. That can expose newly changed, non-public writable fields to the former owner; fetch the full row used for authorization in one statement or keep both reads in a transaction with an appropriate lock.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@aXenDeveloper aXenDeveloper changed the title Add publicWrite feature for member-created content feat: Add publicWrite feature for member-created content Oct 13, 2026
@github-actions github-actions Bot added the 💡 Feature A new feature label Oct 13, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

…edit

- add publicWrite to content types for authenticated public create and edit
- answer public write unique conflicts with structured JSON
- give public write issues a translatable code
- add discussions to the example plugin as the publicWrite reference
- cover publicWrite configuration, routes and permissions
- document publicWrite
- introduce the Frame UI component for structured settings display

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aXenDeveloper
aXenDeveloper force-pushed the claude/content-public-write branch from 52693af to cab4e01 Compare October 13, 2026 11:54
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@aXenDeveloper
aXenDeveloper merged commit edb4d2d into canary Oct 13, 2026
4 checks passed
@aXenDeveloper
aXenDeveloper deleted the claude/content-public-write branch October 13, 2026 11:54
return (
<Frame className="not-prose w-full">
<FrameHeader>
<FrameTitle render={<h3 />}>Publishing</FrameTitle>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/heading-has-content (warning)

Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.

Fix → Put readable text in every heading.

Docs

<div className="not-prose grid w-full gap-4 md:grid-cols-2">
<Frame>
<FrameHeader>
<FrameTitle render={<h3 />}>Personal information</FrameTitle>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/heading-has-content (warning)

Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.

Fix → Put readable text in every heading.

Docs

</Frame>
<Frame>
<FrameHeader>
<FrameTitle render={<h3 />}>Account</FrameTitle>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/heading-has-content (warning)

Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.

Fix → Put readable text in every heading.

Docs

return (
<Frame className="not-prose w-full">
<FrameHeader>
<FrameTitle render={<h3 />}>AI features</FrameTitle>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/heading-has-content (warning)

Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.

Fix → Put readable text in every heading.

Docs

known: readonly string[],
hints: Record<string, string> = {},
): void => {
const unknown = Object.keys(value).find(key => !known.includes(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.

React Doctor · react-doctor/js-set-map-lookups (warning)

This scales poorly because array.includes() inside a loop scans the whole list every time. Use a Set for constant-time lookups.

Fix → Use a Set or Map when you check for the same items over and over. Array.includes/find scans the whole list each time

Docs

);
};

export const ProfileSummary = ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/no-high-complexity-react-function (warning)

ProfileSummary has cyclomatic complexity 14, cognitive complexity 17, and maximum nesting depth 2, so its React logic is hard to understand and change. Extract independent branches into components or hooks.

Fix → Extract independent render branches and state logic into focused components or hooks until the control flow is easy to follow.

Docs

value={user.showRealName ? t("realNameShown") : t("realNameHidden")}
<Frame>
<FrameHeader>
<FrameTitle render={<h2 />}>{t("personalTitle")}</FrameTitle>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/heading-has-content (warning)

Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.

Fix → Put readable text in every heading.

Docs

<FrameTitle
className="flex items-center gap-2"
id={titleId}
render={<h2 />}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/heading-has-content (warning)

Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.

Fix → Put readable text in every heading.

Docs

/>
<Frame aria-labelledby={titleId} render={<section />}>
<FrameHeader>
<FrameTitle id={titleId} render={<h2 />}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/heading-has-content (warning)

Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.

Fix → Put readable text in every heading.

Docs

<Empty className="border">
<EmptyHeader>
<EmptyMedia variant="icon">{icon}</EmptyMedia>
<EmptyTitle render={<h1 />}>{title}</EmptyTitle>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

React Doctor · react-doctor/heading-has-content (warning)

Blind users can't use this heading to navigate because screen readers skip it empty, so add text, aria-label, or aria-labelledby.

Fix → Put readable text in every heading.

Docs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💡 Feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant