Skip to content

feat: public end-user API (/api/public/v1) for native/portal clients#2

Closed
apple-techie wants to merge 18 commits into
mainfrom
feat/public-end-user-api
Closed

feat: public end-user API (/api/public/v1) for native/portal clients#2
apple-techie wants to merge 18 commits into
mainfrom
feat/public-end-user-api

Conversation

@apple-techie
Copy link
Copy Markdown

Summary

Adds a thin public end-user REST API under /api/public/v1/* so native apps (and any first-party client) can read and write feedback as an end user — without a workspace API key. It reuses existing domain services (no new business logic, no schema migrations) and uses the existing better-auth bearer session for writes.

Motivation: the admin /api/v1/* API is API-key/team-scoped (not shippable in a consumer app), and end-user content is otherwise only reachable via the portal's server functions / the widget iframe — neither a public contract. This unblocks a native give-feedback app (a separate iOS repo consumes this via the published OpenAPI spec).

Auth model

  • Reads: anonymous allowed. When a bearer session is present, responses are enriched (e.g. hasVoted).
  • Writes: require a session. requirePortalSession (new domains/api/portal-auth.ts) resolves a better-auth session token → user + principal, mirroring getWidgetSession. The actor is always session.principal.id — never client-supplied.

Endpoints

Reads: GET /config, /boards, /posts (feed, hasVoted), /posts/:id, /posts/:id/comments, /changelog(+/:id), /help/categories, /help/articles/:slug, /help/search, /openapi.json.
Writes (bearer): POST /posts (submit), /posts/:id/vote, /posts/:id/comments.

Visibility / safety (enforced + tested)

  • Feed, detail, and comments only expose public-board, non-deleted, non-merged posts; detail/comments return an identical 404 for private/deleted/merged posts (no existence oracle).
  • Public comment list excludes private/team-only comments (publicOnly option, backward-compatible) and prunes deleted leaves.
  • Changelog/help return published content only.
  • Submit rejects private/nonexistent boards with a uniform 404.

Reuse, not rewrite

Handlers call existing services (createPost, voteOnPost, createComment, listChangelogs, getPublicArticleBySlug, hybridSearch, getPublicWidgetConfig, …). The only new query is listPublicPostFeed (cursor keyset over public boards). The one shared change — adding board.isPublic to PostWithDetails and an optional publicOnly to getCommentsWithReplies — is additive and leaves admin behavior unchanged (verified: admin posts tests pass, tsc clean).

Test plan

  • bunx vitest run over the public API + touched services: 114 tests pass
  • Admin v1/posts tests still pass (10/10) — no regression from shared-service changes
  • tsc --noEmit -p apps/web0 errors (with route tree generated, as CI does)
  • eslint clean on all new files
  • Built via TDD; per-task + holistic review caught and fixed real issues (board-visibility leak, private-comment exposure, deleted/merged exposure, enumeration oracle, nullable-email/empty-returning hardening, author-injection guards)
  • CI green on this PR

🤖 Generated with Claude Code

apple-techie and others added 18 commits May 29, 2026 00:17
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds cursor-keyset paginated public post list filtered to public boards
and non-deleted/non-merged posts, with sort=newest|votes support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ity + rename

Rewrites the relational db.query.* call to db.select().from(posts).innerJoin(boards,...)
so boards.isPublic and boards.deletedAt filters are applied against the correct table.
Renames listPublicPosts → listPublicPostFeed and aligns type names to PublicPostFeedSummary.
Tests are reworked to mock and assert the core query builder chain.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Anonymous-read routes for published changelog entries:
GET /api/public/v1/changelog (hardcoded status:'published', cursor+limit) and
GET /api/public/v1/changelog/:entryId (404 for missing or non-published).
16 vitest tests all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…jection test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ead routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@apple-techie apple-techie requested a review from BunsDev May 29, 2026 17:52
@apple-techie apple-techie self-assigned this May 29, 2026
@BunsDev
Copy link
Copy Markdown
Member

BunsDev commented May 30, 2026

Closing this as superseded/stale after the public mobile API work landed on current main.\n\nThe read surface from this PR now overlaps/conflicts with the merged public API compatibility routes in #3, and the branch no longer merges cleanly. The remaining useful pieces here — portal-session auth, public write endpoints, config/openapi/search/detail expansions — should be re-cut as a smaller follow-up PR against current main so they can be reviewed and verified independently without replaying the already-shipped route work.

@BunsDev BunsDev closed this May 30, 2026
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.

2 participants