DEV Community

Luqman Hakim
Luqman Hakim

Posted on

SonicJS Auth Gotchas on Cloudflare: Signup, Credentials, and RBAC - Part 3

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)
Enter fullscreen mode Exit fullscreen mode

Admins create accounts in Admin → Users → Create New User (/admin/users/new). That path should create both:

  1. the profile row (auth_user)
  2. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Requirements for credentials:

  • Pass ADMIN_EMAIL / ADMIN_PASSWORD via 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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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_user and auth_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

  1. Architecture — Workers + D1 + R2, schema-as-code collections
  2. Deploy + CI/CD — secrets, migrate-then-deploy, GitHub Actions + /health
  3. 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)