Skip to content

feat(create): add React PocketBase add-on - #572

Draft
akanyane wants to merge 2 commits into
TanStack:mainfrom
akanyane:feat/pocketbase-addon
Draft

akanyane wants to merge 2 commits into
TanStack:mainfrom
akanyane:feat/pocketbase-addon

Conversation

@akanyane

@akanyane akanyane commented Nov 6, 2026 •

Copy link
Copy Markdown

Closes #527

Summary

Adds a built-in React add-on for PocketBase at packages/create/src/frameworks/react/add-ons/pocketbase.

tanstack create my-app --add-ons pocketbase (or tanstack add pocketbase) scaffolds:

  • SDK client: pocketbase dependency and src/lib/pocketbase.ts, which exports a browser client (pb) and a createPocketBase() factory for per-request clients in server functions, so auth state is never shared between users on the server.
  • Env: VITE_POCKETBASE_URL in .env.local (defaults to http://127.0.0.1:8090).
  • Migration: pb_migrations/1761955200_created_todos.js creates an owner-scoped todos collection. It's applied automatically by ./pocketbase serve.
  • Gitignore: the pocketbase binary and pb_data/ (local database and uploaded files).
  • Demo route: /demo/pocketbase, with email and password sign-in and a private realtime todo list (create / toggle / delete, live updates across tabs). It uses the standard demo-* classes and theme variables. When the server isn't reachable, it shows setup steps instead of an empty page.
  • README with setup steps and the permission model, plus a changeset.
  • E2E: tests-e2e/pocketbase-smoke.spec.ts, a @matrix smoke test with a local server fixture, following the Strapi one.

Metadata: category database, exclusive with database / orm (same as Convex), file-router mode. React + Vite only (Rsbuild currently accepts toolchain add-ons only).

Versions

  • PocketBase server: v0.40.6
  • JS SDK: pocketbase@^0.28.2

Permissions

Each todo has a required owner relation to PocketBase's built-in users collection. Ownership is enforced by the collection's API rules on the server, not by the client:

Action Rule
List, view, delete @request.auth.id != "" && owner = @request.auth.id
Create @request.auth.id != "" && @request.body.owner = @request.auth.id
Update @request.auth.id != "" && owner = @request.auth.id && (@request.body.owner:isset = false || @request.body.owner = @request.auth.id)
  • The create and update rules also check the submitted owner, so a user can't create a todo for someone else or hand one over.
  • Realtime subscriptions to the collection are checked against listRule, so events only reach the owner.
  • cascadeDelete removes a user's todos when the user is deleted.

PocketBase's default users collection allows public sign-ups, which the demo relies on. The README calls this out, with a pointer to review it before deploying.

Screenshot

2-todos

Local setup and verification

No hosted backend is involved. From a directory next to the repo, after pnpm install && pnpm build:

node cli/packages/cli/dist/index.js create pb-test --add-ons pocketbase --toolchain biome --no-git -y
cd pb-test
# Download PocketBase from https://pocketbase.io/docs/ into this directory, then:
./pocketbase serve            # applies the migration
npm run dev                   # in a second terminal; open /demo/pocketbase

# Permission and SDK checks, with the script below saved as pb-smoke.mjs (Node 24):
node --experimental-eventsource pb-smoke.mjs "$PWD"
pb-smoke.mjs
// Verifies the /demo/pocketbase SDK calls and the `todos` permission model
// against a real PocketBase server started from a generated app.
//
// Usage: node --experimental-eventsource pb-smoke.mjs <path-to-generated-app>
import { createRequire } from 'node:module'
import assert from 'node:assert/strict'

const require = createRequire(process.argv[2] + '/package.json')
const PocketBase = require('pocketbase').default ?? require('pocketbase')

const url = process.env.POCKETBASE_URL ?? 'http://127.0.0.1:8090'
const client = () => {
  const pb = new PocketBase(url)
  pb.autoCancellation(false)
  return pb
}
const wait = (ms = 500) => new Promise((r) => setTimeout(r, ms))
const rejectsWith = (status, promise, label) =>
  assert.rejects(promise, (err) => err.status === status, label).then(() => {
    console.log(`ok: ${label} (${status})`)
  })

async function signUp() {
  const pb = client()
  const email = `smoke-${crypto.randomUUID()}@example.com`
  const password = 'smoke-test-password'
  await pb.collection('users').create({ email, password, passwordConfirm: password })
  const { record } = await pb.collection('users').authWithPassword(email, password)
  return { pb, id: record.id, todos: pb.collection('todos') }
}

const alice = await signUp()
const bob = await signUp()
const guest = client().collection('todos')

const aliceEvents = []
const bobEvents = []
const guestEvents = []
const unsubscribeAlice = await alice.todos.subscribe('*', (e) =>
  aliceEvents.push(`${e.action}:${e.record.text}:${e.record.completed}`),
)
const unsubscribeBob = await bob.todos.subscribe('*', (e) =>
  bobEvents.push(`${e.action}:${e.record.text}`),
)
const unsubscribeGuest = await guest.subscribe('*', (e) =>
  guestEvents.push(`${e.action}:${e.record.text}`),
)

// Owner CRUD, using the same calls as the demo.
const t1 = await alice.todos.create({ text: 'first', completed: false, owner: alice.id })
await wait(20)
const t2 = await alice.todos.create({ text: 'second', completed: false, owner: alice.id })
assert.ok(t1.created, 'autodate created is set')
const sorted = await alice.todos.getFullList({ sort: 'created' })
assert.deepEqual(sorted.map((t) => t.text), ['first', 'second'])
console.log('ok: owner create + list sorted by created')

const updated = await alice.todos.update(t1.id, { completed: true })
assert.equal(updated.completed, true)
console.log('ok: owner update')

await rejectsWith(
  400,
  alice.todos.create({ text: '', owner: alice.id }),
  'empty text rejected',
)

// Another user.
assert.deepEqual(await bob.todos.getFullList(), [])
console.log("ok: another user's list is empty")
await rejectsWith(404, bob.todos.getOne(t1.id), 'another user cannot view')
await rejectsWith(404, bob.todos.update(t1.id, { completed: false }), 'another user cannot update')
await rejectsWith(404, bob.todos.delete(t1.id), 'another user cannot delete')
await rejectsWith(
  400,
  bob.todos.create({ text: 'spoofed', owner: alice.id }),
  'cannot create a todo for another user',
)
await rejectsWith(
  404,
  alice.todos.update(t1.id, { owner: bob.id }),
  'cannot hand a todo over to another user',
)

// Guests.
assert.deepEqual(await guest.getFullList(), [])
console.log('ok: guest list is empty')
await rejectsWith(404, guest.getOne(t1.id), 'guest cannot view')
await rejectsWith(400, guest.create({ text: 'anon', owner: alice.id }), 'guest cannot create')
await rejectsWith(404, guest.update(t1.id, { completed: false }), 'guest cannot update')
await rejectsWith(404, guest.delete(t1.id), 'guest cannot delete')

// The rejected writes changed nothing.
const after = await alice.todos.getOne(t1.id)
assert.equal(after.owner, alice.id)
assert.equal(after.completed, true)
console.log('ok: rejected writes changed nothing')

await alice.todos.delete(t2.id)
console.log('ok: owner delete')

await wait()
assert.deepEqual(aliceEvents, [
  'create:first:false',
  'create:second:false',
  'update:first:true',
  'delete:second:false',
])
console.log('ok: owner receives every realtime event in order')
assert.deepEqual(bobEvents, [])
assert.deepEqual(guestEvents, [])
console.log('ok: other users and guests receive no realtime events')

await unsubscribeAlice()
await unsubscribeBob()
await unsubscribeGuest()

// PocketBase body modifiers must not move a todo either. Some are accepted
// without changing the owner, so check the outcome instead of the status.
for (const body of [{ 'owner+': bob.id }, { '+owner': bob.id }]) {
  await alice.todos.update(t1.id, body).catch(() => {})
  assert.equal((await alice.todos.getOne(t1.id)).owner, alice.id)
}
console.log('ok: owner modifiers cannot hand a todo over')

// Deleting a user removes their todos.
await alice.pb.collection('users').delete(alice.id)
const check = await signUp()
await rejectsWith(404, check.todos.getOne(t1.id), 'todo gone after owner is deleted')

await bob.pb.collection('users').delete(bob.id)
await check.pb.collection('users').delete(check.id)
console.log('ALL OK')

What I ran

  • pnpm build
  • pnpm test:unit: @tanstack/cli 135 of 135. @tanstack/create 434 of 435: edge-import.test.ts hits its 5s timeout in the full suite on my machine and passes alone. It also failed in 1 of 2 runs on a clean main, so it isn't caused by this change.
  • E2E with a local Playwright config, because the repo config needs branded Chrome, which I don't have. It uses headless Chromium instead:
    • the @blocking suite: 14 of 14
    • the new pocketbase-smoke spec: passes
  • In an app from tanstack create: npx tsc --noEmit, npm run lint (Biome) and npm run build
  • In an app from tanstack add pocketbase: npm run build, then npx tsc --noEmit (the route tree is generated by the build)
  • The migration reverts and reapplies with ./pocketbase migrate down 1 and migrate up
  • pb-smoke.mjs against a real PocketBase v0.40.6 server, covering:
    • owner create, list, update and delete, with realtime events delivered in order
    • another user and a guest can't view, update, delete or create for the owner
    • a user can't create a todo for someone else or hand one over, including with the owner+ and +owner modifiers
    • other users and guests receive no realtime events
    • deleting a user removes their todos
  • A local script, not part of this PR, that tries to get around the rules with multipart and @jsonPayload bodies, array values, filters, a forged token and the batch API. No todo changed owner and no other user's data was reachable.
  • The demo in headless Chromium (Playwright), driven by a local script that isn't part of this PR:
    • sync across two tabs of one user, and no todos or events crossing between two users
    • switching user in a tab shows only the new user's todos and stops the previous user's events
    • a wrong password shows an error, a stale session returns to the sign-in form, and a stopped server shows the setup steps

Not covered

  • The new E2E spec uses a fixture, because the PocketBase binary isn't available in CI. It covers generation, type checks, the build and rendering the sign-in form. Permissions are only covered by pb-smoke.mjs, which needs a real server.
  • The commits skipped the pre-commit hook, because it runs the E2E suite against branded Chrome. I ran the same checks as listed above.

Notes

  • The PocketBase binary isn't bundled. The user downloads it, the same manual-backend model as the Strapi add-on.
  • The logo is PocketBase's own logo.svg (MIT).

@coderabbitai

coderabbitai Bot commented Nov 6, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

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

@changeset-bot

changeset-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 56fbd08

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
@tanstack/create Minor
create-start-app Patch
create-tanstack-app Patch
create-tanstack Patch
create-tsrouter-app Patch
@tanstack/create-start Patch
@tanstack/cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@socket-security

socket-security Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedpocketbase@​0.28.210010010092100

View full report

@akanyane
akanyane force-pushed the feat/pocketbase-addon branch from 281a0e6 to 56fbd08 Compare November 6, 2026 20:32
@tannerlinsley

Copy link
Copy Markdown
Member

Thanks, the version details and owner-scoped rules make this much easier to review. Two follow-ups from the draft before the local server checks:

AI review notes:
In PocketBaseDemo, a failed health request sets serverError, and no later path clears it. Starting PocketBase as the setup panel instructs leaves that panel showing indefinitely. Please add a retry action that reruns health/session validation and clears the error on success, with a stopped-server → started-server recovery check. This is a source finding, not a claim that the scenario has been independently run.
The cascade-deletion assertion in pb-smoke.mjs cannot distinguish deletion from the ownership rule: a newly created user already gets 404 for Alice’s todo before Alice is deleted. Please verify existence before deletion and absence afterward through an independent local admin or database view. Keep the separate unauthorized-user 404 test too. The current script is useful permission coverage, but that assertion alone does not establish cascade deletion.
The full local migration, browser/realtime and existing-app verification is still pending on this side; contributor-reported checks remain separate.

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.

Proposal: PocketBase add-on

2 participants