Conversation
…late for the source sqlite database file - dev.sqlite was an old thing
…fies IDE package resolution.
Replaces the inline preview with a Dialog that uses react-easy-crop for square round-shape cropping. Confirm renders the crop region to a canvas, encodes as webp, and feeds the existing upload mutation; the server still handles the final resize and quality reduction.
…nents like buttons with no complaints.
…ion is much better and brighter
Avoids noise in the audit tables by only inserting a purge record when at least one row was actually deleted.
shared/types.d.ts imports FileRouteTypes from client/routeTree.gen, which transitively pulls every client route and its components into the typecheck:shared and typecheck:server programs. Those tsconfigs only aliased @/shared and @/server, so the dragged-in client files failed to resolve their own @/client/* imports. Add @/client/* to both, with comments explaining the import chain.
Eight sequenced feature specs for building out the admin portal on top of Better Auth. 00 lays the foundation (sectioned /admin layout, AdminSection wrapper, ConfirmDialog primitive) that 01-07 build on: users table, full CRUD, impersonation, ban/unban, role management, session management, and admin-set passwords. Each doc lists its dependencies, the Better Auth APIs it uses, files to create/modify, and acceptance criteria.
Restructure /admin from a 2-column AccountCard grid into a sectioned layout (Users on top, System below) so it can host the upcoming users table while keeping the existing Backup and Purge cards intact. Add the shared primitives features 01-07 depend on: - AdminSection: colocated wrapper with title + optional description and right-aligned action slot, reused by both sections. - ConfirmDialog: controlled AlertDialog wrapper using LoadingButton; consumer drives open + isPending from a mutation so async confirms don't auto-close mid-flight. Supports default and destructive variants and an optional body slot for inline form controls. - Install shadcn alert-dialog (declined to overwrite local button.tsx). Exercise ConfirmDialog by gating the Purge Stale Records action behind a destructive confirm.
Adds a paginated, searchable, sortable Users table to the Users section
of /admin — the host surface for every per-user row action that features
02–07 will plug into.
Behavior
- Server-side search (debounced 250ms) with a field switch between
email/name, using better-auth's listUsers `searchOperator: 'contains'`.
- Server-side sort by clicking the Name / Email / Role / Created column
headers; resets to page 1 on sort change.
- Pagination via Prev/Next + "Page X of Y", with 10/25/50 page-size
selector. `offset = (page - 1) * pageSize`, `limit = pageSize`.
- `placeholderData: keepPreviousData` keeps the previous page visible
during refetches so pagination/sort don't flash empty.
- Banned rows are muted with a red Banned badge; hovering the badge
reveals reason + remaining time (or Permanent / Expired).
- Admin users are visually distinct (cyan name + cyan role badge) so the
viewer knows `user:impersonate-admins` will be required later.
- Verified emails show a green BadgeCheck with a tooltip.
State
- Query key: `['admin', 'users', { searchValue, searchField, sortBy,
sortDirection, page, pageSize }]` — features 02–07 invalidate
`['admin', 'users']` on mutation success.
- `currentUser.id` is threaded into `UserRowActions` so future
destructive items can hide themselves on the signed-in row.
Files
- Add src/client/routes/_authenticated/admin/-UsersTable.tsx — table +
header controls (search, field switch, page-size, pagination).
Wraps in a local TooltipProvider since no app-root provider exists.
- Add src/client/routes/_authenticated/admin/-usersTableColumns.tsx —
column defs, SortableHeader, ban-expiry formatter, and the
`TableUser` type derived from `authClient.admin.listUsers`'s return.
- Add src/client/routes/_authenticated/admin/-UserRowActions.tsx —
"..." dropdown trigger with a single disabled placeholder item that
features 02–07 will replace.
- Modify src/client/routes/_authenticated/admin/route.lazy.tsx — mount
<UsersTable /> in the Users section and add a placeholder
"+ Create user" button to the section's action slot (toast for now;
feature 02 wires the dialog).
Dependencies
- Add @tanstack/react-table.
- Install shadcn primitives: table, dropdown-menu, tooltip, select.
Verification
- `bun run typecheck` clean.
- `bunx biome check src/client/routes/_authenticated/admin/` clean.
- No new knip findings.
- Not yet smoke-tested in a browser.
Adds Create / Edit / Delete on user records for admins, all backed by
Better Auth's admin plugin endpoints (no custom Hono routes).
Create (+ Create user button in the Users section header)
- New CreateUserDialog with email, first/last name, password, and role.
- Password field reuses PasswordInput (show/hide) plus a Generate
button that fills a 16-char crypto-random password from a 70-char
pool, and a copy-to-clipboard button that surfaces only after
generation. Password value never appears in toasts or logs.
- Role Select defaults to "user"; options inlined as a tuple that
mirrors userRoles in src/server/constants.ts (client cannot import
from @/server).
- Calls authClient.admin.createUser with lastName under data.
- On success: toast, invalidate ['admin','users'], reset, close.
Edit (row action: Edit user)
- New EditUserDialog editing name and lastName only. Email is not
editable here -- Better Auth's two-step changeEmail flow belongs on
the user's own account page.
- Pre-populates from row data; uses key={user.id} on an inner form
subcomponent so switching rows re-initializes defaults cleanly.
- "Nothing to change" warning when both fields are unchanged.
Delete (row action: Delete user, destructive)
- Hidden when user.id === currentUser.id.
- ConfirmDialog with explicit copy noting the hard delete cascades to
sessions / accounts / passkeys / verification rows and suggests Ban
(plan 04) as the reversible alternative.
- Calls authClient.admin.removeUser.
Shared validators
- Lifted emailValidator, passwordValidator, nameValidator from inside
the auth hook into src/shared/validators.ts so client form
validation and server hook validation share the exact same rules.
ErrorContext
- Added client:adminCreateUser:exception,
client:adminUpdateUser:exception, client:adminRemoveUser:exception
for centralized client-error logging via useLogClientError.
Verified with bun run typecheck (clean across shared/client/server/root)
and biome check on the touched files. Not yet exercised in a browser.
Lets admins masquerade as another user via a row-action in the users
table, and surfaces a sticky banner across the authenticated app so the
admin always knows they're impersonating and can exit in one click.
Server
- src/server/db/auth/auth.ts: explicitly set `impersonationSessionDuration`
on the admin plugin to 60 * 60 (1 hour) so the security choice is
visible in code review rather than relying on the default.
Client
- src/client/components/ImpersonationBanner.tsx (new): sticky amber
banner driven by `authClient.useSession()`. Renders only when
`session.session.impersonatedBy` is truthy. The "Stop impersonating"
button calls `authClient.admin.stopImpersonating()`, then
`router.invalidate()` and navigates to `/admin` to restore the admin's
original view.
- src/client/routes/_authenticated.tsx: mounts <ImpersonationBanner />
above the <Outlet /> so the banner is global across every
authenticated page.
- src/client/routes/_authenticated/admin/-UserRowActions.tsx: adds the
"Impersonate" menu item. Hidden for self-impersonation. Disabled when
the target is an admin and the current admin lacks the
`user:impersonate-admins` permission (checked client-side via
`authClient.admin.checkRolePermission`, no network call). Confirming
in the dialog calls `authClient.admin.impersonateUser({ userId })`,
invalidates the router, and navigates to `defaultAuthedPath`.
- src/shared/types.d.ts: adds `client:adminImpersonateUser:exception`
and `client:adminStopImpersonating:exception` to the `ErrorContext`
union for centralized client error logging.
Notes
- The banner relies on the admin plugin extending the session row with
`impersonatedBy`; `auth.$Infer.Session` already carries it, so no
manual type regeneration was required.
- `stopImpersonating` (not session revoke) is the correct way to exit;
revoking the impersonation session row directly would prevent the
admin's original session from being restored.
Admins can ban a user with an optional reason and duration (1h, 1d, 1w, 30d, custom hours/days, or permanent), or unban a banned user. Banning auto-revokes the user's active sessions and blocks subsequent sign-in with the configured `bannedUserMessage`. The row menu swaps Ban for Unban based on `user.banned`, and both are hidden on the admin's own row.
Adds a Change role row action that opens a ConfirmDialog with a Select body. Calls authClient.admin.setRole and invalidates ['admin', 'users']. - Self is hidden in the row menu to avoid single-admin lockout. - Last-admin guard runs twice: proactively via useQuery so the dialog can render a stronger warning before submit, and authoritatively in the confirm handler to close the TOCTOU window. - Demoting an admin shows a destructive-styled warning in the description; if they're the last admin, the warning escalates and the confirm is blocked with a toast. To honor the plan's "do NOT hardcode role strings" while respecting the biome rule that forbids @/client -> @/server imports, userRoles moves from src/server/constants.ts to src/shared/constants.ts. All five server import sites are updated, and CreateUserDialog drops its inline mirror to use the shared source. Skips the optional refreshUserSessions endpoint: cookieCache.enabled is false (auth.ts:262), so role changes are visible on the next request. Bundled: biome check --write . formatted a few unrelated shadcn UI files (textarea, tooltip, table, select, dropdown-menu) and -BanUserDialog to project style.
Adds a "Manage sessions" row action that opens a dialog listing every active session for the target user. Admins can revoke individual sessions or all sessions at once via a nested confirmation. Impersonation sessions and the viewer's own session are visually distinguished to discourage accidental self-logout.
Adds an admin-driven manual recovery path for setting a user's password
directly, separate from the email-based reset flow.
New `-SetPasswordDialog.tsx`:
- Calls `authClient.admin.setUserPassword`, then conditionally
`revokeUserSessions` — Better Auth's setUserPassword does NOT revoke
existing sessions on its own, so the "Also sign user out of all
devices" checkbox defaults to checked.
- Password generator (16-char, crypto.getRandomValues) with show/hide
toggle and copy-to-clipboard, mirroring CreateUserDialog.
- Real-time "Passwords do not match" validation via tanstack-form's
onChangeListenTo. Submit gated on `length >= minPasswordLength` and
matching confirm.
- After a successful submit the dialog is fully locked until the admin
ticks "I've saved this password": the X is hidden via
`showCloseButton={false}` and ESC / outside-click / overlay-pointer
are all preventDefault'd. Lock state lives in the outer dialog
component so every close path runs through the same guard.
- Success toast is the literal "Password updated" — the password value
never appears in toasts, console output, or logClientError context.
- Invalidates `['admin', 'users']` and
`['admin', 'users', userId, 'sessions']` on success.
- If revokeUserSessions fails after a successful password set, surfaces
a distinct "Password updated, but failed to revoke sessions" toast so
the partial state isn't silently swallowed.
`-UserRowActions.tsx`:
- Adds a "Set password" dropdown item between "Manage sessions" and the
ban items, hidden when `isSelf` (admins should change their own
password through the normal account flow, which preserves session
continuity).
`shared/types.d.ts`:
- Adds `client:adminSetUserPassword:exception` to the ErrorContext
union for `useLogClientError`.
Extract the initials logic into src/client/lib/utils.ts and use it from both userInitialsAtom and the admin users table, replacing the table-local initialsFor utility.
Add a `serverValidationErrorCode` marker that the server attaches to every `APIError` thrown from the Better Auth `before` hook. The client's new `getSafeAuthErrorMessage` helper surfaces the error message only when this marker is present — distinguishing our own validation messages from library-generated ones (e.g. "User already exists") that would leak implementation details. All auth forms now use the helper instead of hardcoded fallback strings. Also switch the admin dialogs from the locally-derived `TableUser` type to the shared `User` type, removing the now-unnecessary re-derivation from `AuthClient['admin']['listUsers']`.
Replaces the users-table-specific UsersSortDirection with a generic SortDirection in src/shared/types.d.ts, reusable by any future sortable table.
Adds null as a third sort state so clicking a sorted-desc column removes the sort rather than snapping back to asc. Also fixes a side-effect-in-updater bug where setSortDirection was called inside setSortBy's pure updater, which React StrictMode could invoke out of order.
Extract the inline search debounce in UsersTable into a reusable useDebouncedValue hook. The optional onDebounced callback fires only when the debounced value actually changes (via an Object.is guard against the previous value), so it does not fire on mount or on no-op re-settles.
Switch the delete-account confirmation input to type=text (so Bitwarden's keyword/type heuristics don't detect it) and reproduce the native bullet masking with CSS -webkit-text-security. Add typeOverride and placeholder props to PasswordInput so all other password fields stay native, and drop the PasswordManagerHint from the delete dialog.
Surface the admin and system audit logs as paginated, sortable, filterable tables in the admin route. Server: - Add GET /admin-audit-logs and /system-audit-logs endpoints with arktype-validated query params (page, pageSize, sortBy, sortDirection, action). Numerics are coerced from query strings via morphs; sorting and filtering on `action` reach into the JSON metadata column via SQLite's `->>` operator. Admin logs inner-join their acting user. - Add GET /avatar/:userId to serve an arbitrary user's avatar, mirroring the authenticated handler (weak ETag, 304 revalidation, no-cache). Client: - Add AdminAuditLogsTable and SystemAuditLogsTable with their column defs, wired into route.lazy via new AdminSection blocks. - Extract SortableHeader into its own component, shared with the users table. - getUserInitials now takes Pick<User, 'name' | 'lastName'>. Shared: - Add arrayOfAll helper for exhaustive, type-checked literal arrays. - Add adminAuditLogActions / systemAuditLogActions as the single source of truth for the audit log action filters. Tests: - Add adminRoutes integration tests plus a bunfig preload that sets the env vars server modules read at module-load time.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Complete ground-up rewrite of
create-new-appfrom v7 → v8. The old CLI is deleted entirely and replaced with a Bun-native TypeScript CLI that generates one of two opinionated, production-ready templates.Scope: 376 files changed, 257 added / 116 deleted / 3 modified (~20k lines in, ~17.6k lines out).
What changed
commander+ EJS + webpack-config-generator pipeline is gone. The new CLI lives insrc/cli/:src/cli/index.ts— entrypoint (#!/usr/bin/env bun), printsintro/outrovia@clack/prompts.src/cli/options-parser.ts—node:utilparseArgswrapper.src/cli/resolveProjectOptions.ts— turns raw args into validated options; prompts only for values that are missing or invalid (unless--yes).src/cli/generateProject.ts— plans the file copy, replaces{{PROJECT_NAME}}/{{BETTER_AUTH_SECRET}}, applies writes, runsbun install/bunx biomeInit/git init.[project-name],--type(fullstack|client-only),--yes,--help,--version. The v7 flag soup (--router,--express,--mongo,--api,--apiPort,--mongoPort,--mongoPortProd,--mongoUser,--mongoAuthSource,--browserslist,--sandbox,--offline,--force,--noGit,--title, etc.) is gone — replaced by two well-tested project shapes.package.jsonexposes bothcreate-new-appandcna, both pointing atsrc/cli/index.ts.src/projects/:fullstack/— React 19 + TanStack Router (file-based, type-safe) + TanStack Query + TanStack Form + Jotai on the client; Bunserve()+ Hono + Drizzle ORM +bun:sqlite+ LiteFS (Fly.io) + Better Auth (with@better-auth/passkey) + Resend / React Email + Sharp on the server. Ships with multi-stageDockerfile,Dockerfile.local,fly.toml,litefs.yml, adeploy.tsscript, and a Drizzle Studio build for prod.client-only-react/— React 19 SPA with the same TanStack stack and Tailwind v4 + shadcn/ui, served by Bun's bundler.package.jsondeclares"workspaces": ["src/projects/*"], so the templates can be developed standalone (cd src/projects/fullstack && bun install) while still being shipped as files.-keeprename trick. Files/dirs that npm strips on publish (.gitignore-keep,.vscode-keep, etc.) or that Bun workspaces would over-eagerly resolve (biome.jsonc-keep,.claude-keep) live with-keepsuffixes in the repo and are renamed on copy bygenerateProject.ts.bun:test.tests/now contains 4 files covering CLI arg parsing, option resolution / prompting, the template planning step, and an end-to-end generate-project integration test. All v7 Jest tests undertests/unit/*.jsare deleted.@qodestack/biome-config) replaces Prettier;bun build --compileproduces a standalone binary for distribution alongside the npm package; TypeScript 6.main.js, all ofmodules/, all offile-creators/, all offiles/(the v7 webpack/EJS template tree including Express, MongoDB, React Router, and sandbox files),checkDependencies.js,removeTestFolders.js,versionCheck.js,.npmignore,.prettierignore,.watchmanconfig. All Express/Mongo/sandbox/webpack code paths are gone.README.mdrewritten to reflect the new architecture, the two-template model, and the new CLI surface. Instructions are Bun-only (bun install -g,bunx); the npm version badge is preserved.Migration note
This is a hard break from v7. Anyone wanting the legacy Express/MongoDB/sandbox CLI should pin to v7. v8 is a different tool published under the same name.
Test plan
bun installat the repo root succeedsbun test— all 4 test files pass (cli.test.ts,resolve-project-options.test.ts,plan-template.test.ts,generate-project.integration.test.ts)bun run check— Biome passes cleanbun run build—bun build --compileproduces a workingdist/create-new-appbinarybun run dev(i.e. running the CLI from source) with no args → guided prompts ask for name + type, then generatebun run dev my-app -t fullstack -y→ no prompts, generates fullstack templatebun run dev my-app -t client-only -y→ no prompts, generates client-only templateMyApp,1app,-app) → validation error or re-prompt--yes→ exits with error--helpand--versionprint and exit 0bun devboots, browser opens, auth flows work, Drizzle Studio reachable viabun run db:viewbun devboots and rendersgit initruns in the generated project (and CLI logs a warning rather than failing ifgitis unavailable){{PROJECT_NAME}}placeholders are replaced everywhere (package.json, fly.toml, litefs.yml, Dockerfile, env files)BETTER_AUTH_SECRETis generated per run for fullstack projects-keepfiles/dirs are renamed on copy (e.g..gitignore-keep→.gitignore,.claude-keep/→.claude/)