react-starter-kit icon indicating copy to clipboard operation
react-starter-kit copied to clipboard

Implement authentication-based home page routing with service bindings

Open koistya opened this issue 4 months ago • 3 comments

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

koistya avatar Aug 10 '25 22:08 koistya

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. 🙂

suhanigupta980 avatar Nov 10 '25 07:11 suhanigupta980

@koistya I’d be happy to work on this. Could I be assigned if this is still open?

reckziegelwilliam avatar Dec 03 '25 06:12 reckziegelwilliam

@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.

koistya avatar Dec 05 '25 15:12 koistya

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.jsonc to add APP_SERVICE + API_SERVICE service bindings, and switch the entry to main: "./worker.ts" (keeping apps/web as the handler for /).

  • Add apps/web/worker.ts (Hono) to make / dynamic:

    • If no Better Auth session cookie → serve marketing (apps/web/dist via ASSETS)
    • If cookie exists → validate via API_SERVICE (/api/auth/get-session)
    • If session is valid → proxy / to APP_SERVICE (apps/app dashboard), otherwise fall back to marketing
    • Fail-safe behavior: on any errors / service downtime, default to marketing
  • Add TypeScript env typings for the new bindings.

  • Test locally with bun web:dev, bun app:dev, bun api:dev and verify:

    • anon → marketing at /
    • authed → app dashboard at /
    • logout / expired session → marketing at /
    • API unavailable → marketing fallback

I’ll open a PR and link it here once it’s ready.

reckziegelwilliam avatar Dec 16 '25 17:12 reckziegelwilliam