ADR-026: V5 Wrapper Persistence Architecture
Status: Accepted Date: 2025-10-13 Decision Makers: Architecture Team Related ADRs:
- ADR-014: Eclipse theia as IDE Foundation
- ADR-001: React + TypeScript for Frontend
- ADR-016: NGINX Load Balancer
Context
The Coditect V5 architecture combines a React-based wrapper (frontend) with Eclipse theia (IDE backend). The wrapper provides critical functionality:
- Navigation: Header with logo, user menu, theme toggle
- Session Management: Multi-tab session switching
- Branding: Coditect AI branding and custom UI
- Authentication: User login, profile, settings
- Documentation: Access to /docs, /support, /faq, etc.
Problem: Users could bypass the wrapper by accessing theia directly:
- Development:
http://localhost:3000(theia port) - Production:
http://coditect.ai/theia(NGINX proxy)
This would result in:
- ❌ Loss of navigation (Header/Footer)
- ❌ No session management UI
- ❌ No branding or custom features
- ❌ Inconsistent user experience
- ❌ Inability to access documentation, settings, profile
Requirement: Ensure the V5 wrapper always remains around theia for navigation and feature access.
Decision
We will enforce wrapper persistence through a multi-layered approach:
1. React Router Architecture
All routes go through the layout component which provides:
- Header (navigation, branding, user menu)
- Main content area (theia embed or page content)
- Footer (links, copyright)
Key Routes:
/ide→ Full theia IDE with wrapper/workspace→ workspace tab with wrapper/ai-studio→ AI Studio tab with wrapper/docs/*→ Documentation pages with wrapper- All routes use
<layout>component
2. Iframe Sandbox Protection
theia is embedded via iframe with strict sandbox rules:
<iframe
src={theiaUrl}
sandbox="allow-same-origin allow-scripts allow-forms allow-modals allow-popups allow-downloads"
// ✅ NO allow-top-navigation - prevents breakout
/>
Security Properties:
- theia cannot navigate parent window
- theia cannot break out of iframe context
- Wrapper remains in control of top-level navigation
- postMessage used for theme sync (controlled communication)
3. NGINX Redirect Protection
Production NGINX configuration prevents direct theia access:
# SECURITY: Prevent direct access to theia
location = /theia {
return 301 /ide; # Redirect to wrapper
}
# API/WebSocket endpoints (iframe only)
location /theia/ {
proxy_pass http://localhost:3000;
# ... WebSocket support
}
Effect:
- ✅
http://coditect.ai/theia→ redirects tohttp://coditect.ai/ide - ✅
http://coditect.ai/theia/services→ proxied (for iframe) - ✅ Users cannot bypass wrapper in production
4. Development Mode Guidance
In development, theia runs on port 3000:
- ✅ Recommended: Use
http://localhost:5173/ide(wrapper + theia) - ⚠️ Not Recommended:
http://localhost:3000(direct theia, no wrapper)
Documentation: Developer guide explains proper access methods.
Architecture Diagram
┌─────────────────────────────────────────────────────────────────┐
│ User Access Points │
├─────────────────────────────────────────────────────────────────┤
│ │
│ http://coditect.ai/ide ✅ Wrapper + theia │
│ http://coditect.ai/workspace ✅ Wrapper + theia Widgets │
│ http://coditect.ai/ai-studio ✅ Wrapper + AI Chat │
│ http://coditect.ai/docs ✅ Wrapper + Documentation │
│ │
│ http://coditect.ai/theia ❌ Redirects to /ide │
│ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ NGINX (Port 80) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ location / { │
│ root /app/v5-frontend; ← V5 React SPA │
│ try_files $uri /index.html; │
│ } │
│ │
│ location = /theia { │
│ return 301 /ide; ← 🔒 REDIRECT PROTECTION │
│ } │
│ │
│ location /theia/ { │
│ proxy_pass http://localhost:3000; ← theia Backend │
│ } │
│ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ V5 React Application │
│ (React Router + layout) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Header (56px) │ │
│ │ - Logo, Navigation, Theme Toggle, User Menu │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Main Content Area │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ theiaEmbed Component (iframe) │ │ │
│ │ │ │ │ │
│ │ │ 🔒 Sandbox: allow-same-origin allow-scripts │ │ │
│ │ │ (NO allow-top-navigation) │ │ │
│ │ │ │ │ │
│ │ │ src="http://localhost:3000?sessionId=xyz" │ │ │
│ │ │ │ │ │
│ │ │ [theia IDE runs inside here] │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Footer (40px) │ │
│ │ - Copyright, Links, Status │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ Eclipse theia Backend │
│ (Port 3000) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ - Monaco editor │
│ - terminal (xterm.js) │
│ - File Explorer │
│ - AI Chat (theia AI extension) │
│ - WebSocket services │
│ │
└─────────────────────────────────────────────────────────────────┘
Implementation Details
File: src/components/layout.tsx
Purpose: Wraps all routes with Header/Footer
export const layout: React.FC<layoutProps> = ({
sessionId,
theiaUrl = 'http://localhost:3000',
children,
showIDE = false,
tabModality = 'theia',
}) => {
return (
<Flex direction="column" h="100vh" w="100vw">
{/* ✅ Header always present */}
<Header />
{/* Main content area */}
<Flex flex={1} overflow="hidden">
{showSidePanel && <SidePanel />}
{renderMainContent()} {/* theia or page content */}
</Flex>
{/* ✅ Footer always present */}
<Footer />
</Flex>
)
}
Effect: Every route gets wrapped, ensuring Header/Footer persistence.
File: src/components/theia-embed.tsx
Purpose: Embeds theia in secure iframe
<Box
as="iframe"
ref={iframeRef}
src={sessionId ? `${theiaUrl}?sessionId=${sessionId}` : theiaUrl}
w="100%"
h="100%"
border="none"
title="theia IDE"
allow="clipboard-read; clipboard-write"
sandbox="allow-same-origin allow-scripts allow-forms allow-modals allow-popups allow-downloads"
// ✅ NO allow-top-navigation - prevents breakout
/>
Security Properties:
allow-same-origin- Enables postMessage communicationallow-scripts- theia needs JavaScriptallow-forms- theia forms (settings, search)allow-modals- theia dialogsallow-popups- theia file downloadsallow-downloads- File download support- ❌ NOT included:
allow-top-navigation(prevents parent navigation)
File: nginx-combined.conf
Purpose: NGINX routing with redirect protection
# SECURITY: Prevent direct access to theia - force users through V5 wrapper
location = /theia {
return 301 /ide; # Redirect to wrapper
}
# API and WebSocket endpoints for theia (accessed via iframe)
location /theia/ {
rewrite ^/theia/(.*) /$1 break;
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# ... other proxy settings
}
Routing Logic:
http://coditect.ai/theia→ 301 redirect to/idehttp://coditect.ai/theia/services→ proxied to theiahttp://coditect.ai/theia/file→ proxied to theia- Root path
/serves V5 React SPA
File: src/app.tsx
Purpose: React Router configuration
<Routes>
{/* Root redirect based on auth */}
<Route path="/" element={isAuthenticated ? <Navigate to="/ide" /> : <Navigate to="/login" />} />
{/* IDE routes with wrapper */}
<Route path="/ide" element={
isAuthenticated ? (
<layout showIDE tabModality="theia" />
) : (
<Navigate to="/login" />
)
} />
<Route path="/workspace" element={
isAuthenticated ? (
<layout showIDE tabModality="workspace" />
) : (
<Navigate to="/login" />
)
} />
{/* Page routes with wrapper */}
<Route path="/docs" element={<layout showIDE={false}><DocumentationPage /></layout>} />
<Route path="/settings" element={<layout showIDE={false}><SettingsPage /></layout>} />
{/* Catch-all */}
<Route path="*" element={<Navigate to="/" />} />
</Routes>
Effect: All routes use <layout>, ensuring wrapper presence.
Security Benefits
1. Consistent User Experience
- Users always see Coditect branding
- Navigation always available (Header)
- Footer links always accessible
2. Session Management
- Multi-session tabs managed by wrapper
- Session switching UI available
- Session state tracked in V5 frontend
3. Authentication Integration
- Protected routes enforced by React Router
- Login/logout handled in wrapper
- User profile/settings accessible
4. Documentation Access
- /docs, /support, /faq available via Header navigation
- Help resources always one click away
- Status page accessible
5. Branding Protection
- Coditect logo and theme always visible
- Custom design system enforced
- Professional appearance maintained
Trade-offs
Advantages
✅ User Experience: Consistent navigation and branding ✅ Security: Controlled access to theia backend ✅ Features: Full V5 features always available ✅ Branding: Coditect identity maintained ✅ Documentation: Help always accessible
Disadvantages
⚠️ Performance: Iframe introduces slight latency (~20-50ms) ⚠️ Complexity: Extra layer between user and theia ⚠️ Debugging: Iframe can complicate DevTools inspection ⚠️ Development: Must use localhost:5173, not localhost:3000
Mitigations
- Performance: Acceptable for browser-based IDE use case
- Complexity: Well-documented architecture, clear separation
- Debugging: Iframe can be inspected via DevTools → Frames
- Development: Documented in developer guide
Alternatives Considered
Alternative 1: Direct theia Access (No Wrapper)
Approach: Users access theia directly at / without wrapper.
Pros:
- Simpler architecture
- Slightly better performance
- No iframe complexity
Cons:
- ❌ No custom branding
- ❌ No navigation to docs/settings
- ❌ No session management UI
- ❌ No authentication integration
- ❌ Loss of V5 features
Rejected: Loses critical business requirements (branding, navigation, features).
Alternative 2: theia Extension for Navigation
Approach: Build theia extensions for Header/Footer instead of wrapper.
Pros:
- Native theia integration
- No iframe needed
- Slightly better performance
Cons:
- ❌ Much more complex to build (3-6 months)
- ❌ Harder to maintain
- ❌ theia updates could break extensions
- ❌ Less flexible than React wrapper
- ❌ Requires deep theia expertise
Rejected: Too complex, slower development, harder to maintain.
Alternative 3: Single-Page App Without Iframe
Approach: Embed theia widgets directly into React components (no iframe).
Pros:
- No iframe overhead
- Seamless integration
- Better performance
Cons:
- ❌ Very complex integration (6-12 months)
- ❌ Requires custom theia build
- ❌ CSS conflicts between React and theia
- ❌ Harder to update theia versions
- ❌ Risk of breaking changes
Rejected: Too complex, high risk, long development time.
Alternative 4: Referer Check (Instead of Redirect)
Approach: Check HTTP Referer header to allow only iframe access.
location /theia {
if ($http_referer !~* "^https?://[^/]+/$") {
return 403; # Forbidden
}
proxy_pass http://localhost:3000;
}
Pros:
- More restrictive than redirect
- Prevents direct browser access
Cons:
- ❌ Referer header can be spoofed
- ❌ Breaks legitimate use cases (e.g., curl, API clients)
- ❌ Browser extensions can strip Referer
- ❌ Less user-friendly (403 error vs redirect)
Rejected: Redirect is more user-friendly and sufficient for our use case.
Testing Strategy
Manual Testing
Test Case 1: Direct /theia Access (Production)
# Expected: Redirect to /ide
curl -I http://coditect.ai/theia
# HTTP/1.1 301 Moved Permanently
# Location: /ide
Test Case 2: Wrapper Navigation
# Expected: V5 wrapper loads, theia in iframe
curl http://coditect.ai/ide
# Returns V5 React SPA HTML
Test Case 3: theia API Access (From Iframe)
# Expected: Proxied to theia backend
curl http://coditect.ai/theia/services
# Returns theia WebSocket endpoint
Test Case 4: Iframe Sandbox (Browser DevTools)
// In theia iframe console:
window.top.location = 'http://evil.com'
// Expected: SecurityError - blocked by sandbox
Automated Testing
Test File: tests/e2e/wrapper-persistence.spec.ts
describe('Wrapper Persistence', () => {
it('redirects /theia to /ide', async () => {
const response = await fetch('/theia', { redirect: 'manual' })
expect(response.status).toBe(301)
expect(response.headers.get('Location')).toBe('/ide')
})
it('shows Header and Footer on /ide', async () => {
await page.goto('/ide')
expect(await page.locator('header').isVisible()).toBe(true)
expect(await page.locator('footer').isVisible()).toBe(true)
})
it('embeds theia in iframe', async () => {
await page.goto('/ide')
const iframe = await page.locator('iframe[title="theia IDE"]')
expect(await iframe.isVisible()).toBe(true)
})
it('prevents iframe from navigating parent', async () => {
await page.goto('/ide')
const iframe = await page.frameLocator('iframe[title="theia IDE"]')
// Try to navigate parent (should fail)
await iframe.evaluate(() => {
try {
window.top.location = 'http://evil.com'
return 'success'
} catch (e) {
return 'blocked'
}
})
expect(result).toBe('blocked')
})
})
Rollout Plan
Phase 1: Development (Complete)
✅ Update nginx-combined.conf with redirect
✅ Verify iframe sandbox attributes
✅ Test wrapper persistence locally
Phase 2: Staging
- Deploy to staging environment
- Run E2E test suite
- Manual QA testing
- Performance validation
Phase 3: Production
- Deploy to production (blue-green rollout)
- Monitor redirect metrics
- Monitor iframe performance
- User feedback collection
Phase 4: Documentation
✅ Create ADR-026 (this document) ✅ Update architecture docs ✅ Update developer guides ✅ Add visual diagrams
Monitoring
Metrics to Track
-
Redirect Rate
- Metric:
nginx_redirect_count{path="/theia"} - Alert: If redirect count > 100/day (users trying to bypass)
- Metric:
-
Iframe Load Time
- Metric:
theia_iframe_load_duration_ms - Target: < 500ms (p95)
- Metric:
-
Wrapper Navigation
- Metric:
v5_header_click_count - Goal: High engagement with wrapper navigation
- Metric:
-
Direct Access Attempts
- Metric:
direct_theia_access_count - Alert: If growing trend (education needed)
- Metric:
Logging
# NGINX access log
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent"';
# Track redirects
access_log /var/log/nginx/access.log main;
Query Example:
# Count /theia redirects per day
grep "GET /theia HTTP" /var/log/nginx/access.log | grep "301" | wc -l
References
- ADR-014: Eclipse theia as IDE Foundation
- ADR-001: React + TypeScript for Frontend
- ADR-016: NGINX Load Balancer
- MDN Iframe Sandbox: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox
- NGINX Rewrite Module: http://nginx.org/en/docs/http/ngx_http_rewrite_module.html
- React Router: https://reactrouter.com/
Appendix A: Configuration Files
nginx-combined.conf (Full)
# NGINX configuration for combined V5 Frontend + theia Backend
server {
listen 80;
server_name _;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript
application/x-javascript application/xml+rss application/json
application/javascript;
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# V5 Frontend (React SPA) - Root path
location / {
root /app/v5-frontend;
try_files $uri $uri/ /index.html;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Don't cache index.html
location = /index.html {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
# SECURITY: Prevent direct access to theia
location = /theia {
return 301 /ide;
}
# theia Backend - API/WebSocket (iframe only)
location /theia/ {
rewrite ^/theia/(.*) /$1 break;
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Proxy headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_read_timeout 86400;
proxy_send_timeout 86400;
proxy_connect_timeout 86400;
# Buffer settings
proxy_buffering off;
proxy_request_buffering off;
}
# theia WebSocket endpoint
location /theia/services {
rewrite ^/theia/(.*) /$1 break;
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
# WebSocket headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# No timeouts for WebSocket
proxy_read_timeout 0;
proxy_send_timeout 0;
}
}
Appendix B: User Documentation
For End Users
How to Access the IDE:
✅ Correct Way:
https://coditect.ai/ide
❌ Incorrect Way (will redirect):
https://coditect.ai/theia
Why the wrapper is important:
- Navigation to documentation, settings, profile
- Session management UI (switch between projects)
- Coditect branding and custom features
- Theme customization
- Help and support access
For Developers
Development Access:
✅ Correct Way (with wrapper):
http://localhost:5173/ide
⚠️ Not Recommended (direct theia, no wrapper):
http://localhost:3000
Why use the wrapper in development?
- Test wrapper integration early
- Verify navigation works
- Catch iframe issues during development
- Matches production behavior
When direct theia access is OK:
- Debugging theia-specific issues
- Testing theia extensions
- theia backend development
Decision
We adopt the multi-layered wrapper persistence architecture:
- ✅ React Router with layout component for all routes
- ✅ Iframe sandbox to prevent theia breakout
- ✅ NGINX redirect to enforce wrapper access
- ✅ Developer documentation for proper usage
Rationale:
- Ensures consistent user experience
- Maintains Coditect branding
- Provides full V5 feature access
- Minimal performance impact
- Simple to implement and maintain
Status: Accepted and implemented (2025-10-13)
Last Updated: 2025-10-13 Version: 1.0 Author: Architecture Team Reviewers: Development Team, Security Team