← Back to Blog
Next.js2026-05-13Β·75 min read

Day 99 of #100DaysOfCode β€” DevCollab: Deploying Next.js and Going Live

By M Saad Ahmad

Cross-Domain Deployment Architecture: Next.js on Vercel + Django on Railway

Current Situation Analysis

Full-stack development often masks critical infrastructure differences between local environments and production. When building locally, the frontend and backend typically share the same origin (localhost), eliminating cross-origin restrictions, simplifying authentication flows, and hiding network latency issues. However, modern deployment strategies frequently decouple these layers, placing the frontend on a specialized edge network like Vercel and the backend on a container platform like Railway.

This separation introduces a "cross-domain" architecture that fundamentally changes how the application behaves. The shift from same-origin to cross-origin triggers strict browser security policies, alters authentication mechanisms, and exposes configuration gaps that remain invisible during local development. Many engineering teams treat deployment as a final step rather than a continuous architectural concern, leading to production failures related to CORS misconfigurations, credential handling errors, and state management race conditions.

Data from deployment logs and incident reports consistently show that cross-origin resource sharing (CORS) errors and environment variable mismatches account for a significant portion of post-deployment bugs. Furthermore, the assumption that local build success guarantees production stability is flawed; production builds involve optimization passes, environment injection, and network routing that can surface latent issues. A robust deployment strategy must account for these divergences, ensuring that the frontend and backend communicate securely and reliably across distinct domains.

WOW Moment: Key Findings

The transition from localhost to a cross-domain production environment introduces measurable changes in behavior, security requirements, and operational complexity. The following comparison highlights the critical differences that impact deployment success.

Dimension Localhost Development Cross-Domain Production (Vercel + Railway)
Origin Policy Same origin; no CORS restrictions. Distinct origins; strict CORS enforcement required.
Authentication Cookies work seamlessly; withCredentials often used. Bearer tokens preferred; withCredentials causes preflight failures.
Environment Variables Single .env file; immediate reload. Split contexts; NEXT_PUBLIC_ prefix required for client vars; Railway auto-redeploys on change.
Build Process Fast incremental builds; no optimization. Full optimization pass; App Router detection; ~2 minute build time.
State Management Instant data availability; no network latency. Async fetches; loading states critical; race conditions possible.
Testing Scope Cached state; local DB; predictable network. Clean state; production DB; real network conditions; incognito testing essential.

Why This Matters: Understanding these differences allows teams to anticipate failure modes before deployment. By addressing CORS, authentication, and state management proactively, developers can reduce post-deployment incidents and ensure a seamless user experience from the first production release.

Core Solution

Deploying a Next.js frontend on Vercel alongside a Django backend on Railway requires a coordinated approach to configuration, security, and state management. The following steps outline the technical implementation, emphasizing architecture decisions and best practices.

1. Frontend Deployment and Environment Configuration

Vercel provides native support for Next.js, automating build detection, optimization, and deployment. The process begins by connecting the GitHub repository to Vercel, which triggers automatic builds on push.

Environment Variable Strategy: The frontend requires the backend's API endpoint to communicate. In Next.js, environment variables exposed to the client must be prefixed with NEXT_PUBLIC_. This prefix signals the build process to inject the value into the client-side bundle.

  • Variable: NEXT_PUBLIC_BACKEND_ENDPOINT
  • Value: The production URL of the Railway backend (e.g., https://your-app.railway.app).
  • Rationale: Using a distinct variable name like NEXT_PUBLIC_BACKEND_ENDPOINT instead of generic names improves clarity and reduces the risk of collisions. The value must be exact, including the protocol (https://) and any required path suffixes, to prevent silent API failures.

Build Optimization: Vercel runs npm run build, detecting the App Router configuration. This process optimizes pages, generates static assets, and prepares the deployment. Running this build locally before deployment validates the configuration and catches errors early.

2. Backend CORS Configuration

Cross-Origin Resource Sharing (CORS) must be explicitly configured on the Django backend to allow requests from the Vercel domain. The django-cors-headers package is typically used for this purpose.

Implementation: Update the CORS_ALLOWED_ORIGINS setting in settings.py to include the Vercel URL. This should be managed via an environment variable in Railway to support dynamic updates without code changes.

# settings.py
import os

# Split comma-separated origins from environment variable
CORS_ALLOWED_ORIGINS = os.getenv("CORS_ALLOWED_ORIGINS", "").split(",")

# Ensure empty strings are filtered out
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in CORS_ALLOWED_ORIGINS if origin.strip()]

Railway Configuration: In the Railway dashboard, set CORS_ALLOWED_ORIGINS to the Vercel URL (e.g., https://your-app.vercel.app). Railway automatically triggers a redeployment when environment variables change, applying the CORS update within minutes.

Rationale: Managing CORS via environment variables enables seamless updates for preview deployments or domain changes. Filtering empty strings prevents configuration errors from blocking all requests.

3. Authentication and Interceptor Configuration

Cross-domain requests complicate authentication. Using cookies with withCredentials: true requires the backend to explicitly allow credentials and match the origin exactly, which can trigger complex preflight requests. A more robust approach for cross-domain architectures is to use Bearer tokens stored in localStorage or a secure cookie strategy managed by the application.

Axios Interceptor Setup: Configure the Axios client to attach Bearer tokens to requests without enabling withCredentials.

// lib/api-client.ts
import axios from 'axios';

const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_BACKEND_ENDPOINT,
  headers: { 'Content-Type': 'application/json' },
});

apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('auth_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  // Explicitly avoid withCredentials for Bearer token auth
  // to prevent unnecessary CORS preflight requests.
  return config;
});

apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Handle token expiration or invalid auth
      localStorage.removeItem('auth_token');
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

export { apiClient };

Rationale: Bearer tokens simplify cross-domain authentication by avoiding the strict requirements of credential-based requests. The interceptor ensures tokens are attached consistently and handles 401 errors gracefully, redirecting users to login when authentication fails.

4. UI State Management and Loading Patterns

Cross-domain requests introduce network latency, requiring explicit handling of loading and error states. A common pitfall is rendering a "not found" message when data is still loading, causing a flash of incorrect content.

Component Implementation: Use a loading state to distinguish between "data not yet fetched" and "data not found".

// app/projects/[id]/page.tsx
import { useProjectQuery } from '@/hooks/use-project-query';
import { SkeletonLoader } from '@/components/skeleton-loader';
import { NotFoundView } from '@/components/not-found-view';
import { ProjectView } from '@/components/project-view';

export default function ProjectDetailPage({ params }: { params: { id: string } }) {
  const { data, isLoading, error } = useProjectQuery(params.id);

  // Show loading skeleton while fetching
  if (isLoading) {
    return <SkeletonLoader />;
  }

  // Show error or not found only after loading completes
  if (error || !data) {
    return <NotFoundView />;
  }

  return <ProjectView project={data} />;
}

Rationale: This pattern prevents the "flash of not found" by ensuring the UI only displays error states after the fetch completes. The loading skeleton provides immediate feedback, improving perceived performance and user experience.

Pitfall Guide

Deploying across distinct providers introduces specific risks. The following pitfalls highlight common mistakes and their resolutions, based on production experience.

Pitfall Explanation Fix
NEXT_PUBLIC_ Secret Leakage Environment variables prefixed with NEXT_PUBLIC_ are embedded in the client bundle. Storing secrets here exposes them to users. Only use NEXT_PUBLIC_ for non-sensitive values like API URLs. Keep secrets in server-side environment variables.
CORS Origin Mismatch The backend rejects requests from unknown origins. Missing the Vercel URL in CORS_ALLOWED_ORIGINS causes silent failures. Add the Vercel URL to CORS_ALLOWED_ORIGINS via Railway env vars. Verify the exact domain, including subdomains.
withCredentials Preflight Hell Setting withCredentials: true on cross-domain requests triggers OPTIONS preflight requests. If the backend doesn't handle credentials, requests fail. Use Bearer tokens instead of cookies for cross-domain auth. Remove withCredentials from Axios config.
Trailing Slash Inconsistency API URLs with or without trailing slashes can cause routing mismatches. https://api.example.com vs https://api.example.com/. Normalize URLs in configuration. Ensure the backend and frontend agree on the exact endpoint format.
Null vs. Loading State Flash Rendering !data as "not found" before the fetch completes causes a brief error flash. Implement explicit loading states. Only show "not found" when isLoading is false and data is null.
Railway Env Var Redeploy Delay Changing environment variables on Railway triggers a redeployment. If the build fails, the app may become unavailable. Monitor deployment logs after env var changes. Test changes in a preview branch if possible.
Cached Browser State Testing on a browser with cached tokens or local storage can mask authentication or state issues. Test on a clean device or incognito window. Verify registration, login, and data flow from scratch.

Production Bundle

This section provides actionable resources for implementing and maintaining the deployment architecture.

Action Checklist

  • Verify Environment Variables: Ensure NEXT_PUBLIC_BACKEND_ENDPOINT is set in Vercel and matches the Railway backend URL exactly.
  • Configure CORS: Add the Vercel domain to CORS_ALLOWED_ORIGINS in Railway and confirm the redeployment succeeds.
  • Review Auth Interceptors: Confirm Axios interceptors use Bearer tokens and do not set withCredentials: true for cross-domain requests.
  • Validate Loading States: Check all data-fetching components for explicit loading and error handling to prevent UI flashes.
  • Test on Clean State: Open the live URL in an incognito window or on a new device to verify registration, login, and core workflows.
  • Inspect Build Logs: Review Vercel build logs for warnings or errors related to environment variables or App Router configuration.
  • Update Documentation: Add the live URL to the GitHub README and repository description for easy access.

Decision Matrix

Scenario Recommended Approach Why Cost Impact
Cross-Domain Auth Bearer Tokens Simplifies CORS configuration; avoids preflight complexity; scalable across domains. No additional cost; reduces infrastructure overhead.
CORS Management Environment Variables Enables dynamic updates without code changes; supports preview deployments. No additional cost; improves operational efficiency.
State Handling Explicit Loading States Prevents UI flashes; improves perceived performance; reduces user confusion. No additional cost; enhances user experience.
Deployment Strategy Vercel + Railway Best-of-breed performance; Vercel optimizes Next.js; Railway simplifies Django/PostgreSQL. Moderate cost; leverages specialized platforms for optimal performance.

Configuration Template

Vercel Environment Variables:

NEXT_PUBLIC_BACKEND_ENDPOINT=https://your-app.railway.app

Railway Environment Variables:

CORS_ALLOWED_ORIGINS=https://your-app.vercel.app
DATABASE_URL=postgresql://user:password@host:port/dbname
SECRET_KEY=your-django-secret-key

Next.js API Client:

// lib/api-client.ts
import axios from 'axios';

const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_BACKEND_ENDPOINT,
  headers: { 'Content-Type': 'application/json' },
});

apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('auth_token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

export { apiClient };

Django CORS Settings:

# settings.py
import os

CORS_ALLOWED_ORIGINS = os.getenv("CORS_ALLOWED_ORIGINS", "").split(",")
CORS_ALLOWED_ORIGINS = [origin.strip() for origin in CORS_ALLOWED_ORIGINS if origin.strip()]

Quick Start Guide

  1. Connect Repositories: Link your GitHub repository to Vercel for the frontend and Railway for the backend.
  2. Set Environment Variables: Configure NEXT_PUBLIC_BACKEND_ENDPOINT in Vercel and CORS_ALLOWED_ORIGINS in Railway with the respective production URLs.
  3. Deploy and Verify: Trigger deployments on both platforms. Monitor Vercel build logs and Railway deployment status for success.
  4. Test Cross-Domain Flow: Open the Vercel URL in an incognito window. Verify registration, login, and API interactions work without CORS errors.
  5. Finalize Documentation: Update the GitHub README with the live URL and deployment instructions. Share the link for stakeholder review.