Authentication and tenancy
How admin and storefront auth work, and how permissions are enforced.
Prolifto uses Better Auth with two separate configurations: one for the admin app and one for the storefront.
Admin authentication
The admin auth instance in @prolifto/auth provides:
- Email and password sign-in with email verification via OTP.
- Two-factor authentication with an authenticator app and backup codes.
- Cloudflare Turnstile captcha on sensitive endpoints.
- Organizations, members, invitations, and roles.
- API keys scoped to an organization.
- Account deletion with an email confirmation link.
It uses the users, sessions, accounts, organizations, members,
invitations, twoFactors, and apiKeys tables.
Storefront authentication
The storefront has its own auth instance and tables:
storefrontUsers,storefrontSessions,storefrontAccounts,storefrontVerifications.- Email and password, email OTP, and Google OAuth.
- Sessions and users are scoped to the organization resolved from the request host.
- Emails are scoped per organization, so the same person can have an account in multiple stores without collisions.
The storefront does not use admin organization roles. Customers are not members of the admin organization.
Organizations and roles
An organization is a store. Membership is represented by the members table
with a role:
| Role | Summary |
|---|---|
owner | Full control, including deleting the store and catalog records |
admin | Day-to-day management, including members, API keys, and settings |
member | Catalog and order work with fewer destructive permissions |
The role statements are defined in
packages/auth/src/utils/permissions.ts. Admin procedures declare their required
permission in apps/admin/orpc/permissions.ts; the permission middleware
enforces it for session-based requests.
Request context
createORPCContext builds the per-request context:
{
db,
headers,
organizationSlug, // first path segment
searchProvider,
session, // Better Auth session, if any
}Procedure types:
publicProcedure— no auth.protectedProcedure— requires a session.orgProcedure— requires a session, active organization, and permission.apiKeyProcedure— accepts anApiKeyheader or a session; API keys bypass the role permission check and are scoped to their organization.
API keys
API keys are created under Settings → API and sent in the ApiKey header:
curl -H "ApiKey: sk_live_..." \
http://localhost:3000/api/my-store/products- Keys are prefixed with
sk_live_and shown once. - The key's organization must match the
{organizationSlug}in the path. - The API key plugin enforces 100 requests per minute per key.
- Plan limits control how many keys a store can create; the Free plan has none.
The storefront uses one key, configured as API_KEY.
Adding a permission
- Add the resource and actions to the statement in
packages/auth/src/utils/permissions.ts. - Assign the actions to the
owner,admin, andmemberroles. - Map the procedure path to a permission in
apps/admin/orpc/permissions.ts. - Update the permission coverage test.
- Update the permissions reference.
Procedures without a mapping fail closed with a "No permission policy is configured" error.
Security notes
- Use the same
BETTER_AUTH_SECRETin the admin and storefront. - Use secure cookies in production; the storefront enables them when configured.
- Keep
TURNSTILE_SECRET_KEYand the site key in sync. - Do not expose admin-only procedures through public routes.
- Treat API keys as secrets. Never log them or commit them.
- Use trusted proxy headers only when the deployment sets them correctly.