Add Authentication to a Frontend Application
Most production applications need authentication. This guide covers three approaches to adding auth to a frontend application on Treasury, with tradeoffs for each.
Choosing an approach
| Approach | Complexity | Cost | Best for |
|---|---|---|---|
| Third-party auth service (Clerk, Auth0) | Low | Per-user pricing | Teams that want auth handled entirely externally |
| Supabase Auth (self-hosted or cloud) | Medium | Free (self-hosted) or usage-based (cloud) | Apps already using Supabase for database/storage |
| Session-based auth with Postgres | Higher | Only Treasury resource costs | Full control over auth logic, no external dependencies |
All three approaches work with any frontend framework deployed on Treasury.
Pattern 1: Third-party auth (Clerk)
Clerk handles user management, sign-up/sign-in UI, session tokens, and multi-factor authentication as an external service. Your Treasury application verifies tokens on each request.
Frontend setup (Next.js example)
Install the Clerk SDK:
Add Clerk's provider to your layout:
Add middleware to protect routes:
Treasury configuration
Set these variables in your Treasury service:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY- your Clerk publishable key (safe for the client).CLERK_SECRET_KEY- your Clerk secret key (server-only, noNEXT_PUBLIC_prefix).NEXT_PUBLIC_CLERK_SIGN_IN_URL- the path for sign-in (e.g.,/sign-in).NEXT_PUBLIC_CLERK_SIGN_UP_URL- the path for sign-up (e.g.,/sign-up).
In Clerk's dashboard, add your Treasury domain to the allowed origins list.
Syncing users to your database
If your app stores user data in Postgres, use Clerk webhooks to sync user creation and updates:
- Create an API route that handles Clerk webhook events.
- In Clerk's dashboard, set the webhook URL to
https://your-app.railway.app/api/webhooks/clerk. - On
user.createdanduser.updatedevents, upsert the user in your Postgres database.
This pattern also works with Auth0 and other providers that offer webhook-based user syncing.
Pattern 2: Supabase Auth
Supabase Auth provides authentication as part of the Supabase platform. You can use Supabase's hosted service or self-host Supabase on Treasury.
Frontend setup
Install the Supabase client:
Initialize the client:
Sign in with email:
Treasury configuration
Set these variables in your Treasury service:
NEXT_PUBLIC_SUPABASE_URL- your Supabase project URL (or your self-hosted instance's public domain).NEXT_PUBLIC_SUPABASE_ANON_KEY- the anonymous/public key (safe for the client).SUPABASE_SERVICE_ROLE_KEY- the service role key (server-only, noNEXT_PUBLIC_prefix). Used for admin operations.
Verifying tokens on your API
If your frontend calls a separate API service on Treasury, verify the JWT on each request:
Pattern 3: Session-based auth with Postgres
For full control, implement session-based authentication using cookies and a Postgres session store. This avoids external dependencies.
Server setup (Express example)
Treasury configuration
DATABASE_URL- reference variable pointing to your Postgres service.SESSION_SECRET- a random string for signing session cookies. Generate one withopenssl rand -base64 32.
Set cookie.secure: true because Treasury terminates TLS at the proxy and forwards requests over HTTP internally. The Secure flag ensures the cookie is only sent over HTTPS connections from the browser.
Common pitfalls
OAuth callback URL mismatch. OAuth providers (Google, GitHub) require an exact callback URL. If your Treasury service uses a generated domain like your-app-production.up.railway.app, add that URL to the provider's allowed redirect URIs. When you add a custom domain, update the redirect URIs to match.
Secret keys in client bundles. Variables like CLERK_SECRET_KEY and SUPABASE_SERVICE_ROLE_KEY must never have a NEXT_PUBLIC_ or VITE_ prefix. These are server-only secrets. If they appear in the browser's JavaScript source, rotate them immediately. See Manage environment variables in frontend builds for the full prefix reference.
Session cookies not sent in cross-origin requests. If your frontend and API are on different Treasury domains, the browser blocks cookies by default. Either deploy both on the same domain (using path-based routing in Caddy, see SPA routing guide) or configure sameSite: 'none' with secure: true on the cookie.
Auth state lost after deploy. If you store sessions in memory (express-session default), all sessions are lost when Treasury deploys a new container. Use a Postgres or Redis session store for persistent sessions.
Next steps
- Manage environment variables in frontend builds - Handle public vs secret keys correctly.
- Postgres on Treasury - Set up a database for user data and sessions.
- Private Networking - Connect frontend and API services securely.
- Configure SPA routing - Serve frontend and API from the same domain.