Single-Page Paid Services on Subdomains: A Deep Dive into Architecture and Implementation
When building focused, monetized digital services, one critical architectural decision emerges early: where should you deploy your service? This question becomes particularly relevant for single-page applications (SPAs) with clear payment flows. Today, we'll explore why subdomains often represent the optimal choice for independent paid services and how to implement this architecture effectively.
Background: The Rise of Focused Paid Services
The modern web has witnessed a shift toward specialized, single-purpose applications. These services typically share common characteristics:
- Clear value proposition: One specific problem solved
- Minimal friction: Often no-signup experiences
- Direct monetization: Pay-per-use or simple subscription models
- Independent operation: Self-contained functionality
Consider services like PDF converters, image processors, or specialized calculators. These applications thrive when they can operate independently while maintaining connection to their parent brand.
Core Architectural Concepts
Subdomain vs. Subdirectory: The Fundamental Choice
When deploying a paid service, you face a fundamental architectural decision:
Subdirectory Approach (example.com/tool)
example.com/
├── / (main site)
├── /tool (paid service)
└── /api (shared backend)
Subdomain Approach (tool.example.com)
example.com (main site)
tool.example.com (independent service)
pay.example.com (payment processor)
Why Subdomains Excel for Paid Services
1. Clear Responsibility Boundaries
Subdomains create natural architectural isolation:
// Frontend deployment isolation
tool.example.com → Vercel/Cloudflare Pages
main.example.com → Different deployment pipeline
// Backend isolation
tool-api.example.com → Independent serverless functions
main-api.example.com → Main application backendThis separation provides several advantages:
- Risk isolation: Payment issues don't affect the main site
- Technology flexibility: Each service can use optimal tech stacks
- Independent scaling: Resources allocated per service needs
2. Mental Model Alignment
From a user experience perspective, subdomains communicate purpose effectively:
www.company.com→ Brand and informationtool.company.com→ Focused functionalitypay.company.com→ Transaction processing
This mental model reduces cognitive load and increases conversion rates.
Implementation Analysis
Recommended Architecture Pattern
Here's a battle-tested structure for single-page paid services:
Architecture Layout:
Main Domain (www.example.com):
- Brand presence
- Trust signals
- Documentation
- Customer support
Service Subdomain (tool.example.com):
- Core functionality
- User interface
- Local state management
- Payment initiation
Payment Subdomain (pay.example.com):
- Stripe Checkout integration
- Payment processing
- Success/failure handling
- Webhook endpointsCode Implementation Examples
Frontend Service Structure
// tool.example.com - Main service file
import { loadStripe } from '@stripe/stripe-js';
class PaymentService {
constructor() {
this.stripe = loadStripe(process.env.STRIPE_PUBLISHABLE_KEY);
}
async initiatePayment(productData) {
try {
// Create checkout session
const response = await fetch('/api/create-checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
product: productData,
success_url: `${window.location.origin}/success`,
cancel_url: `${window.location.origin}/cancel`,
}),
});
const { sessionId } = await response.json();
const stripe = await this.stripe;
// Redirect to Stripe Checkout
await stripe.redirectToCheckout({ sessionId });
} catch (error) {
console.error('Payment initiation failed:', error);
}
}
}Backend Payment Processing
// Cloudflare Worker for payment processing
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/api/create-checkout' && request.method === 'POST') {
return await handleCheckoutCreation(request, env);
}
if (url.pathname === '/api/webhook' && request.method === 'POST') {
return await handleStripeWebhook(request, env);
}
return new Response('Not found', { status: 404 });
}
};
async function handleCheckoutCreation(request, env) {
const { product, success_url, cancel_url } = await request.json();
const session = await env.STRIPE.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: product.name,
},
unit_amount: product.price * 100, // Convert to cents
},
quantity: 1,
}],
mode: 'payment',
success_url: success_url,
cancel_url: cancel_url,
metadata: {
service: 'tool-service',
subdomain: 'tool.example.com'
}
});
return Response.json({ sessionId: session.id });
}Security and Compliance Considerations
Cross-Origin Resource Sharing (CORS)
When working with subdomains, CORS configuration becomes crucial:
// Configure CORS for subdomain communication
const corsHeaders = {
'Access-Control-Allow-Origin': 'https://tool.example.com',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true'
};
// Handle preflight requests
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: corsHeaders
});
}Cookie and Session Management
Subdomain cookie handling requires careful consideration:
// Set cookies for subdomain scope
function setSubdomainCookie(name, value, domain) {
document.cookie = `${name}=${value}; Domain=.example.com; Path=/; Secure; SameSite=Strict`;
}
// Alternative: Use URL parameters for state transfer
function redirectWithState(targetUrl, state) {
const url = new URL(targetUrl);
url.searchParams.set('state', btoa(JSON.stringify(state)));
window.location.href = url.toString();
}Critical Implementation Challenges
1. State Management Across Subdomains
Challenge: Maintaining user context across subdomain boundaries.
Solution: Implement signed token-based state transfer:
// Generate signed state token
async function generateStateToken(data, secret) {
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const key = await crypto.subtle.importKey(
'raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
);
const payload = btoa(JSON.stringify(data));
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(payload));
return `${payload}.${btoa(String.fromCharCode(...new Uint8Array(signature)))}`;
}
// Verify state token
async function verifyStateToken(token, secret) {
const [payload, signature] = token.split('.');
// Verification logic here
return JSON.parse(atob(payload));
}2. Payment Webhook Security
Challenge: Ensuring webhook authenticity across subdomain architecture.
Solution: Implement robust webhook verification:
async function verifyStripeWebhook(request, endpointSecret) {
const body = await request.text();
const sig = request.headers.get('stripe-signature');
try {
const event = stripe.webhooks.constructEvent(body, sig, endpointSecret);
// Additional metadata verification
if (event.data.object.metadata?.service !== 'tool-service') {
throw new Error('Invalid service metadata');
}
return event;
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
throw err;
}
}3. SEO and Discovery Implications
Challenge: Subdomains are treated as separate domains by search engines.
Mitigation Strategies:
- Implement canonical linking between main domain and subdomains
- Create clear navigation paths from main site
- Use structured data to indicate service relationships
<!-- On main domain -->
<link rel="alternate" href="https://tool.example.com" title="Tool Service" />
<!-- On subdomain -->
<link rel="canonical" href="https://www.example.com/tools/tool-name" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebApplication",
"name": "Tool Service",
"url": "https://tool.example.com",
"provider": {
"@type": "Organization",
"name": "Example Company",
"url": "https://www.example.com"
}
}
</script>Implications and Best Practices
When Subdomains Are Optimal
Subdomains work best for:
- Independent functionality: Clear, self-contained services
- Pay-per-use models: One-time or session-based payments
- Rapid iteration: Services requiring frequent updates or A/B testing
- Risk isolation: Services with different compliance or security requirements
When to Consider Alternatives
Avoid subdomains when:
- Shared user accounts: Deep integration with existing user systems
- SEO dependency: Organic search is the primary acquisition channel
- Complex user journeys: Multi-step processes spanning different services
Deployment Best Practices
Infrastructure as Code
# Cloudflare configuration example
services:
main-site:
domain: www.example.com
type: static
source: ./main-site
tool-service:
domain: tool.example.com
type: static
source: ./tool-service
environment:
STRIPE_PUBLISHABLE_KEY: ${STRIPE_PUB_KEY}
payment-api:
domain: api.example.com
type: worker
source: ./payment-worker
secrets:
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY}
WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET}Monitoring and Analytics
Implement comprehensive monitoring across your subdomain architecture:
// Unified analytics tracking
class SubdomainAnalytics {
constructor(config) {
this.config = config;
this.subdomain = window.location.hostname;
}
trackEvent(eventName, properties) {
// Add subdomain context to all events
const enrichedProperties = {
...properties,
subdomain: this.subdomain,
service: this.config.serviceName,
timestamp: new Date().toISOString()
};
// Send to analytics service
this.sendToAnalytics(eventName, enrichedProperties);
}
trackPaymentFlow(stage, metadata) {
this.trackEvent('payment_flow', {
stage,
...metadata,
flow_id: this.generateFlowI