Implement authentication-based home page routing with service bindings
Overview
Implement dynamic home page routing based on user authentication status, similar to how GitHub serves different content on the root URL (/) for authenticated vs non-authenticated users. This will be achieved using Cloudflare Service Bindings to connect our independently deployed workers.
Architecture Update
Important: The apps/edge/ package has been removed and consolidated into apps/api/. We now have three independently deployable workers:
apps/web/- Marketing site (static assets)apps/app/- Main application (SPA with routes like /settings, /analytics)apps/api/- Backend API (tRPC endpoints)
Current Behavior
The root URL (/) is handled by the apps/web/ worker, which serves static marketing content regardless of authentication status.
Desired Behavior
- Unauthenticated users visiting
/should see the marketing site (apps/web/dist/index.html) - Authenticated users visiting
/should see the main application dashboard (served byapps/app/)
This creates a seamless GitHub-like experience where the home page intelligently adapts based on user state.
Technical Implementation
Approach: Web Worker as Authentication Router
The apps/web/ worker will be enhanced to act as an authentication-aware router for the home page, using service bindings to delegate to the appropriate worker.
1. Update apps/web/wrangler.jsonc
Add service bindings to connect to the app and API workers:
{
// ... existing config
"services": [
{
"binding": "APP_SERVICE",
"service": "example-app" // Name from apps/app/wrangler.jsonc
},
{
"binding": "API_SERVICE",
"service": "example-api" // Name from apps/api/wrangler.jsonc
}
],
// ... rest of config
}
2. Create apps/web/worker.ts
Implement a worker that checks authentication and routes accordingly:
import { Hono } from 'hono';
import { createAuth } from '@repo/api/auth';
import { createDb } from '@repo/api';
import { getCookie } from 'hono/cookie';
interface Env {
APP_SERVICE: Fetcher;
API_SERVICE: Fetcher;
HYPERDRIVE_CACHED: Hyperdrive;
BETTER_AUTH_SECRET: string;
// ... other env vars
}
const app = new Hono<{ Bindings: Env }>();
// Home page routing logic
app.get('/', async (c) => {
try {
// Check if user has a session cookie
const sessionToken = getCookie(c, 'better-auth.session_token');
if (!sessionToken) {
// No session cookie, serve marketing site
return c.env.ASSETS.fetch(c.req.raw);
}
// Verify session with API service
const authCheckResponse = await c.env.API_SERVICE.fetch(
new Request('https://internal/api/auth/get-session', {
headers: {
'Cookie': `better-auth.session_token=${sessionToken}`,
'Content-Type': 'application/json'
}
})
);
if (authCheckResponse.ok) {
const { session } = await authCheckResponse.json();
if (session) {
// Valid session, proxy to app service
return c.env.APP_SERVICE.fetch(c.req.raw);
}
}
// Invalid session or error, serve marketing site
return c.env.ASSETS.fetch(c.req.raw);
} catch (error) {
// On any error, default to marketing site
console.error('Auth check failed:', error);
return c.env.ASSETS.fetch(c.req.raw);
}
});
// Serve all other routes from static assets
app.get('*', (c) => c.env.ASSETS.fetch(c.req.raw));
export default app;
3. Update apps/web/wrangler.jsonc entry point
Change the main entry to use the worker:
{
"main": "./worker.ts", // Add this
"assets": {
"directory": "./dist"
},
// ... rest of config
}
4. Add TypeScript definitions
Create apps/web/types/env.d.ts:
interface Env {
ASSETS: Fetcher; // Built-in for static assets
APP_SERVICE: Fetcher;
API_SERVICE: Fetcher;
BETTER_AUTH_SECRET: string;
ENVIRONMENT: string;
}
Alternative Approach: App Worker as Router
An alternative would be to have apps/app/ handle the routing logic and delegate to apps/web/ for unauthenticated users. This approach would:
- Keep authentication logic closer to the application
- Require updating routes in
apps/app/wrangler.jsoncto capture/ - Use service binding to fetch marketing content from
apps/web/
Implementation Considerations
Performance
- Service bindings have zero latency overhead (run on same thread)
- Consider caching session validation results in Workers KV for faster subsequent checks
- Set appropriate cache headers based on authentication state
Security
- Ensure session validation cannot be bypassed
- Use secure cookie settings for session tokens
- Handle expired/invalid sessions gracefully
- Never expose internal service binding URLs
Deployment
- Service bindings require workers to be deployed in order
- First deploy
apps/api/andapps/app/ - Then deploy
apps/web/with service bindings configured - Use consistent service names across environments
Testing
- Test both authenticated and unauthenticated scenarios
- Verify proper fallback when services are unavailable
- Ensure session state is maintained across navigation
- Test with expired/invalid session tokens
Acceptance Criteria
- [ ] Unauthenticated users see marketing content at
/ - [ ] Authenticated users see application dashboard at
/ - [ ] Session validation is performed securely
- [ ] Proper error handling when services are unavailable
- [ ] No breaking changes to existing routes
- [ ] Performance impact is minimal (< 50ms additional latency)
- [ ] Works across all environments (dev, staging, production)
Resources
- Cloudflare Service Bindings Documentation
- Better Auth Session Management
- Hono Cookie Helper
- Current auth implementation:
apps/api/lib/auth.ts
Development Setup
-
Ensure all three workers are running locally:
bun web:dev # Port 5173 bun app:dev # Port 5174 bun api:dev # Port 5175 -
Test authentication flow:
- Visit
http://localhost:5173(should see marketing site) - Log in via
/loginroute - Visit
/again (should see app dashboard) - Log out and verify
/shows marketing site again
- Visit
Notes
- This implementation maintains the independence of all three workers
- Each worker can still be deployed separately
- Service bindings provide secure, performant inter-worker communication
- The approach mimics GitHub's seamless authenticated/unauthenticated experience
This is a great issue for learning:
- Cloudflare Workers service bindings
- Authentication flow in distributed systems
- Dynamic routing based on user state
- Modern edge computing patterns
Hi! I would like to work on this issue.
I have read the description and understand that the goal is:
- Route "/" based on authentication state
- Serve marketing site for unauthenticated users (apps/web)
- Serve application dashboard for authenticated users (apps/app) using service bindings
I will implement the worker-based routing approach as described:
- Update wrangler.jsonc with APP_SERVICE and API_SERVICE bindings
- Create apps/web/worker.ts for auth-aware routing
- Add type definitions for Env bindings
Please assign this issue to me. 🙂
@koistya I’d be happy to work on this. Could I be assigned if this is still open?
@reckziegelwilliam sounds good, that would be great! Please, let me know if you bump into any issues.
@suhanigupta980 sorry, I missed your comment but you're also welcome to participate in this.
Hey @koistya — I’m going to take this one.
I’ll implement the “apps/web as auth-aware router” approach described in the issue:
-
Update
apps/web/wrangler.jsoncto addAPP_SERVICE+API_SERVICEservice bindings, and switch the entry tomain: "./worker.ts"(keepingapps/webas the handler for/). -
Add
apps/web/worker.ts(Hono) to make/dynamic:- If no Better Auth session cookie → serve marketing (
apps/web/distviaASSETS) - If cookie exists → validate via
API_SERVICE(/api/auth/get-session) - If session is valid → proxy
/toAPP_SERVICE(apps/app dashboard), otherwise fall back to marketing - Fail-safe behavior: on any errors / service downtime, default to marketing
- If no Better Auth session cookie → serve marketing (
-
Add TypeScript env typings for the new bindings.
-
Test locally with
bun web:dev,bun app:dev,bun api:devand verify:- anon → marketing at
/ - authed → app dashboard at
/ - logout / expired session → marketing at
/ - API unavailable → marketing fallback
- anon → marketing at
I’ll open a PR and link it here once it’s ready.