Someone tries to log in and you get one of:
- Credential account not found
- Login works, but You do not have permission to access this area
- Random people registering themselves as editors
This post is the production auth/ops guide for SonicJS on Workers — the stuff that isn’t in the “hello deploy” tutorial.
1. Turn off public self-registration
For an internal CMS, open signup is a footgun. Disable Better Auth sign-up in app config:
import { createSonicJSApp, registerCollections } from '@sonicjs-cms/core'
import type { SonicJSConfig } from '@sonicjs-cms/core'
import blogPostsCollection from './collections/blog-posts.collection'
registerCollections([blogPostsCollection])
const config: SonicJSConfig = {
plugins: { register: [] },
auth: {
extendBetterAuth: (opts) => ({
...opts,
emailAndPassword: {
...opts.emailAndPassword,
disableSignUp: true,
},
}),
},
}
export default createSonicJSApp(config)
Admins create accounts in Admin → Users → Create New User (/admin/users/new). That path should create both:
- the profile row (
auth_user) - the Better Auth credential row (
auth_account)
…so the new user can log in immediately.
2. Two password stores (this is the big one)
SonicJS login (Better Auth) reads credentials from auth_account.
Some UI/profile flows only update auth_user.password_hash.
If those diverge, you get:
Credential account not found
even though the user “exists” in the admin list.
Fix: reset password with a script that updates both
Prefer a CLI over the admin password form when login is broken:
ADMIN_EMAIL=you@example.com ADMIN_PASSWORD='your-new-password' npm run set-password:prod
That script should upsert the Better Auth credential row and keep the profile hash in sync.
For brand-new environments, seed once with env vars — never commit passwords:
ADMIN_EMAIL=you@example.com ADMIN_PASSWORD='your-secure-password' npm run seed:prod
A solid seed uses Wrangler’s platform proxy against production D1:
import { getPlatformProxy } from 'wrangler'
const useProduction = process.argv.includes('--remote')
const { env, dispose } = await getPlatformProxy({
environment: useProduction ? 'production' : undefined,
remoteBindings: useProduction,
})
// env.DB is your D1 binding — insert auth_user + auth_account, assign RBAC, etc.
Requirements for credentials:
- Pass
ADMIN_EMAIL/ADMIN_PASSWORDvia env - Enforce min password length
- Exit early if the admin already exists (idempotent seed)
3. RBAC ≠ auth_user.role
Even after login succeeds, /admin may show:
You do not have permission to access this area
Classic cause: the user has a string role on auth_user (e.g. editor), but no RBAC role assignment that grants portal:access.
Older admin UI flows sometimes wrote only auth_user.role. Portal access is enforced via RBAC.
Fix: promote the user
# default: editor with portal access
ADMIN_EMAIL=user@example.com npm run promote-user:prod
# full admin
ADMIN_EMAIL=user@example.com ROLE=admin npm run promote-user:prod
After you patch/upgrade core so “Create New User” assigns RBAC automatically, new users are fine — but existing accounts may still need a one-time promote.
4. Ops cheat sheet
| Symptom | Likely cause | Fix |
|---|---|---|
| Anyone can register | Sign-up still enabled |
disableSignUp: true in extendBetterAuth
|
| “Credential account not found” | Missing/outdated auth_account row |
set-password:prod for that email |
| Login OK, no portal access | Missing RBAC / portal:access
|
promote-user:prod |
| Seed failed: no DB binding | Wrong env / migrations | Check wrangler.toml production D1 + remote = true; run migrations |
| Auth weird after redeploy | Secrets missing/rotated badly | Re-put BETTER_AUTH_SECRET / JWT_SECRET
|
5. Beta packages and patches (optional honesty)
SonicJS moves fast (@sonicjs-cms/core betas). If you hit a framework bug in user-create or login UI, patch-package can unblock production while you wait for upstream:
{
"scripts": {
"postinstall": "patch-package"
}
}
Keep patches small and documented. Prefer upstream upgrades when the fix lands — patches are a bridge, not a lifestyle.
6. Hardening checklist for a private CMS
- [ ] Public signup disabled
- [ ] First admin seeded via env (not hardcoded)
- [ ] Create-user flow writes
auth_userandauth_account - [ ] Editors/admins have RBAC with
portal:access - [ ] Password resets go through a script that updates both stores
- [ ] Production secrets set via
wrangler secret put - [ ] Collections that should be public API opt in with
access.public: ['read'](from Part 1) — everything else stays private
Series wrap-up
- Architecture — Workers + D1 + R2, schema-as-code collections
-
Deploy + CI/CD — secrets, migrate-then-deploy, GitHub Actions +
/health - This post — signup lock, credential rows, RBAC
That’s a complete path from “edge CMS idea” to “team can log into /admin without Slack-debugging auth.”
If you ship something similar, start with SonicJS’s docs, then treat auth as a first-class production concern — not a day-two chore.
Further reading: SonicJS · Better Auth · Workers CI with GitHub Actions
Top comments (0)