Authentication Setup
Creating a Clerk application, getting your API keys, and configuring the session token for role-based access.
Clerk is the managed authentication provider used by this template. It handles sign-up, sign-in, session management, OAuth providers (Google, GitHub, etc.), and user profiles - all without you building or maintaining any auth infrastructure. The free tier covers most early-stage projects.
Consult the Clerk documentation if you require more information
Create a Clerk application
- Go to dashboard.clerk.com and create a new application
- Navigate to API Keys and copy:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYCLERK_SECRET_KEY
Add both to .env.local.
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is inlined into the client bundle at next build, not read at runtime. This works fine on Vercel (each environment
builds separately). If you build a single Docker image and promote it across
environments, the key is baked in at build time and the runtime value never
reaches the bundle. See the self-hosting note in src/app/layout.tsx if
that's your deployment model.
Route gating
Auth is enforced in src/proxy.ts (Next.js 16 renamed middleware.ts to proxy.ts to
make the network boundary explicit). It redirects unauthenticated users to /sign-in
and gates app pages by role.
The proxy is only the first line - a coarse routing gate. Every API route
and server action independently re-checks auth (defence-in-depth), so a
change to the proxy can never silently expose the database. See
API Layer → Auth for the
withAuth / withRole wrappers that do this.
The live demo deployment of this template is intentionally locked to
role-holding users only. If you sign in and find yourself on an "Access
restricted" page, that's expected - the deployment is a private testing
environment, not a public product. When scaffolding a real project from this
template, relax the gate in proxy.ts to allow broader access.
The two-role model
The template ships two roles with a hierarchy. This
exists to make the authentication-vs-authorization split concrete: withAuth
answers "are you signed in?", withRole answers "are you allowed to do this?".
| Role | Gets into the app | Can read | Can write (POST/PATCH/DELETE) |
|---|---|---|---|
Admin | yes | yes | no |
SuperAdmin | yes | yes | yes |
SuperAdmin is a strict superset of Admin - it can do everything an Admin
can, plus write. Anywhere the code checks a role it uses a hierarchy
(hasRole), never string equality, so a SuperAdmin automatically satisfies
any Admin requirement. When you add a new gated capability, remember that
SuperAdmin inherits it by default.
- Proxy (
proxy.ts) - decides page access only. BothAdminandSuperAdminpass; anyone else lands on/restricted. - API routes + server actions - reads require any signed-in Admin+
(
withAuth); writes requireSuperAdmin(withRole('SuperAdmin')). A non-SuperAdmin write returns a 403 (routes) or a toasted error (actions).
Gating every write behind SuperAdmin is a teaching choice for the template
- it gives you two test users to exercise both the allowed and denied paths. In
a real product you would pick the role per route rather than blanket-
SuperAdminevery mutation. See API Layer → Auth for the full pattern.
Why the routes use [[...sign-in]]
Clerk's prebuilt UI handles multiple authentication methods - email/password, magic links, OAuth providers (Google, GitHub, etc.) - and each step in each flow lands on a different URL path:
/sign-in
/sign-in/factor-one
/sign-in/oauth/google/start
/sign-in/oauth/github/callback
/sign-in/sso-callbackRather than creating a separate Next.js page file for every possible path, the routes use Next.js optional catch-all segments:
app/(auth)/sign-in/[[...sign-in]]/page.tsxThe [[...sign-in]] segment matches /sign-in and any sub-path beneath it - so one
page file covers the entire authentication flow without any additional routing config.
Clerk's component handles the internal navigation between steps.
Granting a role (Admin / SuperAdmin)
App access and the API write gate are driven by a role claim in the session
token. Two steps are required - missing either gives a silent 403.
To exercise both paths, set up two users - one Admin and one
SuperAdmin. The Admin gets into the app and can read but sees a 403 / "you
do not have the correct role" toast on any create/edit/delete; the SuperAdmin
can do everything.
If you still get a 403 after both steps, the claim is missing from the JWT - check that step 2 was saved, or that the session isn't stale (sign out and back in).
E2E test users
Playwright drives the role model with three users, one per persona. Create them the easy
way - sign in to the running app as each account (that auto-creates it), then grant the
role metadata in the Clerk Dashboard as above (SuperAdmin, Admin, and no role key
for the third). Set their emails in .env.local:
E2E_CLERK_USER_EMAIL_SUPER_ADMIN=superadmin@yourdomain.com
E2E_CLERK_USER_EMAIL_ADMIN=admin@yourdomain.com
E2E_CLERK_USER_EMAIL_NO_ROLE=norole@yourdomain.comThe superAdminPage, adminPage, and noRolePage fixtures each sign in as the matching
user; roles.spec.ts uses all three to prove the page gate and API/action authz.
See Testing for the full e2e auth setup.