Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ src/gatsby-types.d.ts
.idea/*
**/*.swp
.claude
.env
29 changes: 29 additions & 0 deletions poc-nextjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# dependencies
node_modules
.pnpm-debug.log*

# next.js
.next/
out/

# production
build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
159 changes: 159 additions & 0 deletions poc-nextjs/POC_RESULTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# POC T003: API Key Injection (Next.js) - Results

## Verdict: VALIDATED

The API key injection pattern from the Gatsby Docs site can be successfully replicated in Next.js 15 with App Router, with no hydration mismatches or significant session issues.

## Evidence

### 1. Session Cookie Handling
- **Route Handler** (`/api/user`): Successfully reads cookies from incoming requests
- **Rails API Integration**: Proxies requests to Rails `/api/me` and `/api/api_keys` endpoints
- **Logged-out state**: Returns empty user data, triggers demo key fallback
- **Cookie forwarding**: All cookies from the request are forwarded to Rails

```typescript
// From app/api/user/route.ts
const cookieStore = await cookies();
const allCookies = cookieStore.getAll();
const cookieHeader = allCookies
.map(({ name, value }) => `${name}=${value}`)
.join('; ');
```

### 2. API Key Injection
- **Original code**: `import.meta.env.VITE_ABLY_KEY`
- **After injection**: `"xVLyHw.DQrNxQ:..."` (demo key for logged-out users)
- **Endpoint injection**: Adds `endpoint: 'sandbox'` for non-production environments

The `updateAblyConnectionKey` utility was ported successfully from Gatsby:
- Replaces `import.meta.env.VITE_ABLY_KEY` with actual API key
- Injects Ably endpoint for non-production environments
- Supports additional key replacements

### 3. Sandpack Integration
- **Renders correctly**: Code editor and preview both functional
- **File tabs**: Working (index.js, index.html)
- **Preview iframe**: Shows "Ably Pub/Sub Demo" interface
- **Dependencies**: Installed via Sandpack's bundler

### 4. Hydration Safety
- **No hydration mismatches**: Verified in browser console (0 errors, 0 warnings)
- **Hydration marker**: Confirms `data-hydration="complete"`
- **Strategy**: Client-side data fetching with loading state prevents SSR/client mismatch

```tsx
// Hydration-safe pattern
const [userData, setUserData] = useState<UserDetails | null>(null);
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
// Fetch on client only - no SSR data means no mismatch
fetchUserData();
}, []);

if (isLoading) return <LoadingState />; // Same on server and initial client render
```

### 5. Logged-in vs Logged-out States

| State | Banner | API Key | Works |
|-------|--------|---------|-------|
| Logged out | "Not logged in - using demo API key" | Demo Key (xVLyHw...) | Yes |
| Logged in | "Logged in as {name} - using your real API key" | User's key | Not testable locally* |

*Manual testing with real Rails session required for logged-in state.

## Screenshots

### Example Page - Logged Out State
![POC Example Page](./screenshots/poc-example.png)
- Shows "Not logged in - using demo API key" banner
- API Key displays "Demo Key (xVLyHw...)"
- Sandpack renders correctly
- Hydration marker shows "complete"

### Code Editor - API Key Injection
![Code Editor with Injected Key](./screenshots/poc-indexjs.png)
- Line 5: `endpoint: 'sandbox'` - environment injection
- Line 6: `key: "xVLyHw.DQrNxQ:..."` - API key injection

## Browser Console Output

```
Console errors: []
Console warnings: []

No hydration warnings found in console
```

## Build Output

```
Route (app) Size First Load JS
--- / 3.46 kB 105 kB
--- /_not-found 996 B 103 kB
--- /api/user 127 B 102 kB
--- /example 218 kB 319 kB
```

Build completed successfully with no TypeScript or compilation errors.

## Learnings

### What Works Well
1. **Next.js App Router** handles cookies() async correctly in route handlers
2. **Sandpack with 'use client'** works without hydration issues when data fetching is client-side
3. **Demo key fallback** provides seamless experience for logged-out users
4. **Environment variable injection** (endpoint) works alongside API key injection

### Considerations for Production
1. **CORS**: Cross-origin requests to Rails may need CORS headers configured
2. **Cookie sharing**: Subdomain cookies (e.g., `*.ably.com`) should work if properly configured
3. **ISR/Caching**: Pages with API keys should NOT be statically cached - the example page uses client-side fetching which avoids this issue
4. **Error handling**: Network failures gracefully fall back to demo key

### Potential Issues Identified
1. **Rails session cookie httpOnly**: The cookie is readable server-side (route handler), but not in client JavaScript - this is actually correct and secure behavior
2. **CORS for subdomains**: Would need `credentials: 'include'` and proper Access-Control headers on Rails
3. **Sandpack bundle size**: 218 kB for the example page - acceptable for docs

## Recommendations

1. **Proceed with migration**: The core pattern works. API key injection in Next.js App Router is validated.

2. **Keep client-side fetching**: The pattern of fetching user data client-side avoids hydration mismatches and caching issues with API keys.

3. **Test with real session**: Before full migration, test with actual Rails session to verify:
- Cookie domain sharing across subdomains
- CORS configuration
- Logged-in user flow

4. **Consider caching strategy**:
- Static pages: Use ISR for content
- API key pages: Keep client-side fetching (no caching of user-specific data)

## Time Taken

- **Estimated**: 1-2 days
- **Actual**: ~4 hours

## Files Created

```
poc-nextjs/
--- app/
--- --- api/user/route.ts # Session proxy route handler
--- --- example/page.tsx # Sandpack with API key injection
--- --- layout.tsx # Root layout
--- --- page.tsx # Homepage
--- lib/
--- --- update-ably-connection-keys.ts # Ported utility
--- package.json
--- tsconfig.json
--- next.config.ts
--- eslint.config.mjs
--- .env.local
--- .gitignore
--- POC_RESULTS.md # This file
```
90 changes: 90 additions & 0 deletions poc-nextjs/app/api/user/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { NextRequest, NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import type { UserDetails, SessionState, App } from '@/lib/update-ably-connection-keys';

// Rails API base URL - matches the Gatsby configuration
const RAILS_API_BASE = process.env.NEXT_PUBLIC_ABLY_MAIN_WEBSITE || 'https://ably.com';

/**
* API Route Handler: GET /api/user
*
* This route proxies requests to the Rails session endpoints to fetch:
* 1. Session state (/api/me) - user authentication status and profile
* 2. API keys (/api/api_keys) - user's Ably applications and API keys
*
* The Rails session cookie is automatically forwarded because we're reading
* cookies from the incoming request and including them in our fetch calls.
*
* This pattern enables:
* - SSR-compatible session validation
* - API key injection in Sandpack without client-side cookie exposure
* - Unified session handling across Docs (Next.js) and Dashboard (Rails)
*/
export async function GET(request: NextRequest) {
try {
// Read the cookies from the incoming request
// This will include the Rails session cookie if user is logged in
const cookieStore = await cookies();
const allCookies = cookieStore.getAll();

// Format cookies for forwarding to Rails
const cookieHeader = allCookies
.map(({ name, value }) => `${name}=${value}`)
.join('; ');

// Log for debugging (remove in production)
console.log('[/api/user] Cookie names present:', allCookies.map(c => c.name));

// Fetch session state from Rails
const sessionResponse = await fetch(`${RAILS_API_BASE}/api/me`, {
headers: {
Cookie: cookieHeader,
Accept: 'application/json',
},
// Important: include credentials to forward cookies
credentials: 'include',
});

let sessionState: SessionState = {};
if (sessionResponse.ok) {
sessionState = await sessionResponse.json();
console.log('[/api/user] Session signedIn:', sessionState.signedIn);
} else {
console.log('[/api/user] Session response status:', sessionResponse.status);
}

// Fetch API keys from Rails (only if signed in)
let apps: App[] = [];
if (sessionState.signedIn) {
const keysResponse = await fetch(`${RAILS_API_BASE}/api/api_keys`, {
headers: {
Cookie: cookieHeader,
Accept: 'application/json',
},
credentials: 'include',
});

if (keysResponse.ok) {
apps = await keysResponse.json();
console.log('[/api/user] API keys found:', apps.length);
} else {
console.log('[/api/user] API keys response status:', keysResponse.status);
}
}

const userData: UserDetails = {
sessionState,
apps,
};

return NextResponse.json(userData);
} catch (error) {
console.error('[/api/user] Error:', error);

// Return empty user data on error - will fall back to demo key
return NextResponse.json({
sessionState: {},
apps: [],
} satisfies UserDetails);
}
}
Loading