Title: feat(create): add React PocketBase add-on by akanyane · Pull Request #572 · TanStack/cli · GitHub
Open Graph Title: feat(create): add React PocketBase add-on by akanyane · Pull Request #572 · TanStack/cli
X Title: feat(create): add React PocketBase add-on by akanyane · Pull Request #572 · TanStack/cli
Description: 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 and a "Try again" button instead of an empty page. README with setup steps and the permission model, plus a changeset. E2E: tests-e2e/pocketbase-smoke.spec.ts, @matrix tests with a local server fixture, following the Strapi one. They cover rendering the sign-in form and recovering after the server starts. 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 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 superuser upsert smoke-admin@example.com smoke-test-password # used by pb-smoke.mjs ./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, from the generated app and before `./pocketbase serve`: // ./pocketbase superuser upsert smoke-admin@example.com smoke-test-password // node --experimental-eventsource pb-smoke.mjs "$PWD" // // The superuser ignores API rules, so it can tell a deleted record from one // that is only hidden by them. Set POCKETBASE_SUPERUSER_EMAIL and // POCKETBASE_SUPERUSER_PASSWORD to use other credentials. 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, and only theirs. Checked through a // superuser, because a 404 for a regular user would also come from the rules. const admin = client() await admin .collection('_superusers') .authWithPassword( process.env.POCKETBASE_SUPERUSER_EMAIL ?? 'smoke-admin@example.com', process.env.POCKETBASE_SUPERUSER_PASSWORD ?? 'smoke-test-password', ) const allTodos = admin.collection('todos') const bobTodo = await bob.todos.create({ text: 'bob', owner: bob.id }) assert.equal((await allTodos.getOne(t1.id)).owner, alice.id) assert.equal((await allTodos.getOne(bobTodo.id)).owner, bob.id) console.log('ok: superuser sees both todos before the deletion') await alice.pb.collection('users').delete(alice.id) await rejectsWith(404, allTodos.getOne(t1.id), "superuser: deleted user's todo is gone") assert.deepEqual(await allTodos.getFullList({ filter: `owner = "${alice.id}"` }), []) console.log("ok: superuser finds no todos for the deleted user") assert.equal((await allTodos.getOne(bobTodo.id)).owner, bob.id) console.log("ok: another user's todo survives the deletion") await bob.pb.collection('users').delete(bob.id) await rejectsWith(404, allTodos.getOne(bobTodo.id), "superuser: second user's todo is gone") 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 2 pocketbase-smoke specs: pass 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 and leaves other users' todos, checked through a local superuser before and after the deletion. With cascadeDelete: false in the migration, the script fails at that deletion, so the check does detect a missing cascade. 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, and a stale session returns to the sign-in form with the real server stopped, the setup panel appears and "Try again" keeps it. After starting the server, "Try again" clears it, both signed out and with an existing session, whose todos and writes still work 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, rendering the sign-in form and recovery. 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). Summary by CodeRabbit New Features Added a PocketBase add-on for React TanStack Start apps, including a sign-in and account-creation demo with a private, real-time todo list. Added owner-scoped todo access and setup guidance for connecting to a local PocketBase server. Documentation Documented PocketBase installation, configuration, demo usage, and security considerations. Tests Added end-to-end checks for sign-in and retrying when the PocketBase server is unavailable.
Open Graph Description: 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) ...
X Description: 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) ...
Opengraph URL: https://github.com/TanStack/cli/pull/572
X: @github
Domain: github.com
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| route-pattern | /:user_id/:repository/pull/:id/files(.:format) |
| route-controller | pull_requests |
| route-action | files |
| fetch-nonce | v2:b2551288-b922-c859-ced5-740cb9e3671c |
| current-catalog-service-hash | ae870bc5e265a340912cde392f23dad3671a0a881730ffdadd82f2f57d81641b |
| request-id | E300:349533:254F6EF:31082ED:6AF99E5B |
| html-safe-nonce | a4dd056256222f74bd212e4e4fcbad9f4598397d279df03714f58c382fa6af8e |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJFMzAwOjM0OTUzMzoyNTRGNkVGOjMxMDgyRUQ6NkFGOTlFNUIiLCJ2aXNpdG9yX2lkIjoiNDQ5ODIwOTE2MDcxMzc0NDI3IiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | b3d6620d310a0558b78b4946d7afd7f84744ee3120f97f55f7672fe9a741a716 |
| hovercard-subject-tag | pull_request:5194018740 |
| github-keyboard-shortcuts | repository,pull-request-list,pull-request-conversation,pull-request-files-changed,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/TanStack/cli/pull/572/files |
| twitter:image | https://avatars.githubusercontent.com/u/61711487?s=400&v=4 |
| twitter:card | summary_large_image |
| og:image | https://avatars.githubusercontent.com/u/61711487?s=400&v=4 |
| og:image:alt | 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) ... |
| og:site_name | GitHub |
| og:type | object |
| hostname | github.com |
| expected-hostname | github.com |
| None | 612d3e6ffd693e7ea8d9fcf452370995872baef31d825b1fedef704cd3d24ac7 |
| turbo-cache-control | no-preview |
| diff-view | unified |
| go-import | github.com/TanStack/cli git https://github.com/TanStack/cli.git |
| octolytics-dimension-user_id | 72518640 |
| octolytics-dimension-user_login | TanStack |
| octolytics-dimension-repository_id | 932949058 |
| octolytics-dimension-repository_nwo | TanStack/cli |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 932949058 |
| octolytics-dimension-repository_network_root_nwo | TanStack/cli |
| turbo-body-classes | logged-out env-production page-responsive full-width |
| disable-turbo | true |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 072e34035c13c31543fba9f025cbbbda4210dae4 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width