EEPNext.js Template
Dev Environment

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

  1. Go to dashboard.clerk.com and create a new application
  2. Navigate to API Keys and copy:
    • NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
    • CLERK_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?".

RoleGets into the appCan readCan write (POST/PATCH/DELETE)
Adminyesyesno
SuperAdminyesyesyes

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. Both Admin and SuperAdmin pass; anyone else lands on /restricted.
  • API routes + server actions - reads require any signed-in Admin+ (withAuth); writes require SuperAdmin (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-SuperAdmin every 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-callback

Rather 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.tsx

The [[...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.com

The 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.

On this page