Skip to content

Can't login as admin in production #1054

Open

Description

@hankeyyh

Problem

I follow the production deployment,deploy on cloudflare, add admin secret

echo "admin@yourdomain.com" | wrangler secret put ADMIN_EMAIL --env production
echo "your-secure-password" | wrangler secret put ADMIN_PASSWORD --env production
openssl rand -base64 32 | wrangler secret put JWT_SECRET --env production
openssl rand -base64 32 | wrangler secret put BETTER_AUTH_SECRET --env production

But when i try to login, it reports Invalid email or password.

Goal

Login as admin in production.

Effort

I check remote auth_user, auth_account tables,no entries found. Then i use the sql from seed-admin.ts

INSERT INTO auth_user (id, email, first_name, last_name, role, is_active, created_at, updated_at, name) VALUES(xxx)
INSERT INTO auth_account (id, user_id, account_id, provider_id, password, created_at, updated_at) VALUES(xxx)

Now it reports You do not have permission to access this area. I think it's because the missing of rbac record. But seed-admin.ts use rbac service to add user role. I don't know how to deal with it in sql.

const rbac = new RbacService(env.DB)
await rbac.ensureSystemRbacSeed()
await rbac.addUserRoleByName(odid, 'admin')

Activity

  1. mmcintosh commented on Aug 21, 2026

    @mmcintosh
    Collaborator

    This is very likely the same root cause as the Better Auth 1.7 float tracked in #1061, with the fix already open in #1062.

    packages/core/package.json pins Better Auth with a caret ("better-auth": "^1.6.13"), so a fresh install with no lockfile — a production deploy or a new consumer — resolves to the current latest, 1.7.1:

    $ echo '{"dependencies":{"better-auth":"^1.6.13"}}' > package.json && npm i --package-lock-only
    # → better-auth 1.7.1
    

    Better Auth 1.7.0 has breaking schema changes — notably "Accounts now require Account.issuer… Must apply account-identity backfill before deploying" — but 0001_core.sql was written for 1.6.x, and its auth_account table has no issuer column. So on any deploy that floats to 1.7, the account/login/session paths fail at runtime while the rest of the app works — which matches "can't login as admin in production." (Installs that kept a 1.6.x lockfile are unaffected, which is why it's intermittent by environment.)

    The fix is #1062 — pin the Better Auth family to an exact, schema-compatible 1.6.25. It's open but hasn't had its CI authorized or a review yet.

  2. elpastiche commented on Sep 14, 2026

    @elpastiche

    We hit this exact sequence on 3.0.0-beta.28 and got all the way through it. Posting the full path because, after the Better Auth 1.7 float in #1061 is pinned, there are two more independent blockers — including the one that left you stuck at You do not have permission to access this area.

    Everything below was reproduced with better-auth already pinned to 1.6.31, so none of it is the 1.7 issue.

    Blocker 2 — the web registration form creates unusable accounts

    The register page posts via htmx to POST /auth/register/form, not to /auth/register. That form endpoint inserts into auth_user but never creates the auth_account row with provider_id = 'credential'. Login reads the password from auth_account, so accounts created through the UI can never log in.

    Reproduced side by side on a clean database:

    # what the browser actually does
    curl -X POST $URL/auth/register/form -b cookies.txt -H "X-CSRF-Token: $CSRF" \
      --data-urlencode "firstName=Form" --data-urlencode "lastName=Endpoint" \
      --data-urlencode "email=formtest@example.com" --data-urlencode "password=FormTest12345"
    # -> HTTP 200
    
    # the JSON endpoint
    curl -X POST $URL/auth/register -b cookies.txt \
      -H 'Content-Type: application/json' -H "X-CSRF-Token: $CSRF" \
      -d '{"email":"jsontest@example.com","password":"JsonTest12345","firstName":"Json","lastName":"Endpoint"}'
    # -> HTTP 201

    Resulting rows:

    formtest@example.com    credential accounts = 0
    jsontest@example.com    credential accounts = 1
    

    Logging in as the form-created user returns 401 {"error":"Invalid email or password"}, and the worker log says:

    WARN [Better Auth]: Credential account not found
    

    auth_user.failed_login_count stays at 0, because the password is never compared — a useful signal for telling this apart from a genuinely wrong password.

    Two things make this painful to diagnose:

    1. Registration returns 200, so it looks like it worked.
    2. The account cannot be repaired by hand. auth_user.password_hash stores pbkdf2:100000:<salt>:<hash> while auth_account.password stores Better Auth's scrypt <salt>:<hash>. The formats differ, so you cannot copy the hash between tables — the only recovery is deleting the user and recreating them through the JSON endpoint.

    Workaround: create users against POST /auth/register instead of the form.

    Blocker 3 — RBAC is unseeded, which is your You do not have permission error

    Admin access is not governed by auth_user.role. It is governed by document-backed RBAC. On our fresh install the document types were registered but zero documents existed:

    SELECT type_id, count(*) FROM documents WHERE type_id LIKE 'rbac%' GROUP BY type_id;
    -- 0 rows
    
    SELECT id, is_auth FROM document_types WHERE id LIKE 'rbac%';
    -- rbac_role       | 1
    -- rbac_verb       | 1
    -- rbac_user_roles | 1

    With role = 'admin' and is_super_admin = 1 set on the user, login succeeded and /admin/* still 403'd:

    POST /auth/login/form -> Ok
    GET  /admin/content   -> 403
    GET  /auth/login?error=You%20do%20not%20have%20permission%20to%20access%20this%20area
    

    This looks like the same root cause as #1017 (first-boot FK race leaves RBAC unseeded, and the KV skip-marker latches the broken state). Worth noting we had just created a fresh KV namespace, which matches that description.

    How we fixed it. Running the library's own seeding against the database resolves it:

    await bootstrapDocumentTypes(env.DB)
    const rbac = new RbacService(env.DB)
    await rbac.ensureSystemRbacSeed()
    await rbac.addUserRoleByName(userId, 'admin')

    That produces 4 rbac_role, 6 rbac_verb and 1 rbac_user_roles documents. Verified end to end with no other change: /admin/content was 403 before and 200 after.

    If you cannot run TypeScript against the remote D1 (getPlatformProxy({remote:true}) needs remote = true on each binding plus an experimental flag, otherwise it silently uses the local database), you can seed by SQL. The linking detail that is easy to miss: the rbac_user_roles document's slug column holds the user id, and its data is {"roleIds":["role-admin"]}. Role and verb documents are referenced by stable slugs (role-admin, verb-read, …), not by their random document ids.

    Suggestion

    Given how many installs will walk this same path, it might be worth having the deployment docs create the first admin through the JSON endpoint and verify RBAC seeding, until #1017 and #1061 land.

    Environment: @sonicjs-cms/core@3.0.0-beta.28, create-sonicjs starter, Cloudflare Workers + D1 (remote production and local miniflare), wrangler 4.131.2, node 22.22.3.

  3. ruledweb commented on Oct 10, 2026

    @ruledweb

    I tried recently in local server and found issue in installed package.

    Better Auth rejected the tenant tables in the installed @sonicjs-cms/core package, organizationId was mapped to the SQL column tenant_id instead of the Drizzle field tenantId, and updatedAt was required even though Better Auth never writes it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions