Authentication
User authentication and authorization
Overview
EasyStarter uses Better Auth for authentication, providing a complete solution for user management.
Supported Methods
| Method | Description |
|---|---|
| Email/Password | Traditional registration with email verification |
| GitHub OAuth | One-click login with GitHub account |
| Google OAuth | One-click login with Google account |
Configuration
Environment Variables
# Required
AUTH_SECRET=your-secret-key
# OAuth (optional)
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Email (for verification)
RESEND_API_KEY=your-resend-api-keyFrontend Usage
Sign In
import { authClient } from "@/lib/auth-client";
// Email/Password
await authClient.signIn.email({
email: "user@example.com",
password: "password123",
});
// OAuth
await authClient.signIn.social({ provider: "github" });
await authClient.signIn.social({ provider: "google" });Sign Out
await authClient.signOut();Get Current User
const { data: session } = authClient.useSession();
const user = session?.user;Session Management
Sessions are automatically managed by Better Auth:
- Cookie-based session storage
- Automatic refresh before expiration
- Cross-tab synchronization
Protected Routes
Use the authentication middleware to protect routes:
// In your route component
import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/dashboard")({
beforeLoad: async ({ context }) => {
if (!context.user) {
throw redirect({ to: "/sign-in" });
}
},
});Roles and permissions
EasyStarter uses a static global role matrix in @repo/app-config/rbac with two roles: user and admin. An account holds exactly one role. Roles describe authority; they are not Membership Tiers.
RBAC is already integrated with authentication, Web, Native, and server-side oRPC. There is no separate rbac.enabled switch.
To adopt it, enable the admin features, configure an administrator email, and let that account complete a verified sign-in.
Enable admin features in appConfig
Update appConfig.common in packages/app-config/src/app-config.ts:
common: {
admin: {
paidUsers: {
enabled: true,
},
userManagement: {
enabled: true,
},
},
auth: {
// Other authentication settings…
rbac: {
defaultRole: "user",
adminRoles: ["admin"],
},
},
}admin.userManagement.enabled controls user-management navigation, pages, and server APIs. admin.paidUsers.enabled controls the paid-user page and APIs.
When disabled, the related feature returns NOT_FOUND instead of only hiding its menu item.
Keep defaultRole set to "user" so new accounts have no administrative permissions. Do not change it to "admin" to enable RBAC, because that would make every new account an administrator.
adminRoles: ["admin"] tells Better Auth which roles are administrative. The project currently declares only the user and admin roles.
Configure the initial administrator
Set ADMIN_EMAIL to the real account email that will sign in as an administrator. For example:
ADMIN_EMAIL=admin@yourcompany.comThis is not the sender address or supportEmail. It must match the account's sign-in email, and that account must have completed email verification.
Matching is case-insensitive, and surrounding whitespace is removed from the configured value.
ADMIN_EMAIL currently accepts one email only. Do not provide a comma-separated list. After the initial administrator signs in, use the user-management page to assign the admin role to other accounts.
For local development, copy the example file and set the email:
cp apps/server/.dev.vars.example apps/server/.dev.varsADMIN_EMAIL=admin@yourcompany.comFor production, use a server secret:
cp apps/server/.env.production.example apps/server/.env.productionADMIN_EMAIL=admin@yourcompany.comUpload the completed file to Cloudflare:
pnpm -F server secrets:bulk:productionActivate the administrator role
- Start or redeploy the Server.
- Complete email verification for the account that matches
ADMIN_EMAIL. - Sign out and sign in again to create a new session.
- Before creating the session, the Server updates that account's
roletoadmin.
This works for new and existing accounts. An existing account is promoted on its next verified sign-in.
Removing or changing ADMIN_EMAIL does not revoke an existing administrator. Change the old administrator's role back to user before updating the variable.
While the old email remains configured, signing in again promotes that account back to admin.
Default permission matrix
| Permission | Purpose | user | admin |
|---|---|---|---|
admin:access | Enter administrative areas | ✗ | ✓ |
user:list | List users | ✗ | ✓ |
user:set-role | Change user roles | ✗ | ✓ |
user:ban | Ban and unban users | ✗ | ✓ |
credits:adjust | Adjust credits | ✗ | ✓ |
membership:grant-trial | Grant a Membership trial | ✗ | ✓ |
operation:list | View administrative operations | ✗ | ✓ |
User management
The user-management surface reads and updates real accounts from the configured database. Only accounts with the required RBAC permissions can list users, change roles, or update ban status.
The template does not generate synthetic users automatically.
Protect a new API with RBAC
For a general administrative endpoint, use adminProcedure:
import { adminProcedure } from "@/lib/orpc";
export const someAdminAction = adminProcedure.handler(async () => {
// Only accounts with admin:access can execute this procedure.
});For a more specific permission, call assertPermission inside a protected procedure:
import { assertPermission, protectedProcedure } from "@/lib/orpc";
export const adjustCredits = protectedProcedure.handler(async ({ context }) => {
assertPermission(context, "credits", "adjust");
// Business logic
});Web and Native can use hasPermission(role, resource, action) to hide unauthorized entry points or redirect to a forbidden page. Client checks improve the experience, but the server must always enforce permissions independently.
Closed Better Auth admin HTTP routes
The Better Auth admin plugin is registered for role storage, ban semantics, and server-side auth.api.* calls. Its /api/auth/admin/* HTTP surface is rejected in apps/server/src/index.ts on purpose.
Administration goes through oRPC procedures (users.list, users.setRole, users.ban, users.unban). Do not call authClient.admin.* against this server.