Prolifto
Developer guide

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:

RoleSummary
ownerFull control, including deleting the store and catalog records
adminDay-to-day management, including members, API keys, and settings
memberCatalog 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 an ApiKey header 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

  1. Add the resource and actions to the statement in packages/auth/src/utils/permissions.ts.
  2. Assign the actions to the owner, admin, and member roles.
  3. Map the procedure path to a permission in apps/admin/orpc/permissions.ts.
  4. Update the permission coverage test.
  5. 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_SECRET in the admin and storefront.
  • Use secure cookies in production; the storefront enables them when configured.
  • Keep TURNSTILE_SECRET_KEY and 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.

On this page