Skip to content

Implement authentication-based home page routing with service bindings #2101

@koistya

Description

@koistya

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 by apps/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.jsonc to 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/ and apps/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

Development Setup

  1. Ensure all three workers are running locally:

    bun web:dev  # Port 5173
    bun app:dev  # Port 5174  
    bun api:dev  # Port 5175
  2. Test authentication flow:

    • Visit http://localhost:5173 (should see marketing site)
    • Log in via /login route
    • Visit / again (should see app dashboard)
    • Log out and verify / shows marketing site again

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    Ready

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions