Makuhari Development Corporation
6 min read, 1165 words, last updated: 2026/1/4
TwitterLinkedInFacebookEmail

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 backend

This 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 information
  • tool.company.com → Focused functionality
  • pay.company.com → Transaction processing

This mental model reduces cognitive load and increases conversion rates.

Implementation Analysis

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 endpoints

Code 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
  });
}

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
Makuhari Development Corporation
法人番号: 6040001134259
サイトマップ
ご利用にあたって
個人情報保護方針
個人情報取扱に関する同意事項
お問い合わせ
Copyright© Makuhari Development Corporation. All Rights Reserved.