Headless commerce separates the shopping experience from the commerce engine. Your storefront — a Next.js site, a mobile app, a kiosk, an in-game shop — talks to a commerce API over HTTP. The backend handles products, carts, orders, payments, and fulfillment without caring what's rendering them.
Medusa is one of the more capable open-source options in this space: a Node.js commerce engine with modular services, a REST API, and an admin dashboard. Because it's stateless at the API layer, it scales horizontally in a way that monolithic platforms usually don't.
This guide covers what actually matters when running Medusa in production — the architecture, the stateful dependencies, the configuration that breaks deployments, and the operational concerns that surface after launch. It's written to be useful regardless of which specific Medusa release you're on.
TL;DR: Medusa needs Postgres for persistent data and benefits significantly from Redis for caching and background jobs. Deployment means connecting a repository, configuring environment variables (especially CORS, which breaks more launches than anything else), running migrations before new containers serve traffic, and setting scaling bounds. The stateless API layer scales horizontally; the database is your real capacity constraint.
Understanding the Architecture
Before deploying anything, it helps to be clear about what's stateless and what isn't — that distinction drives every scaling decision you'll make later.
Component | State | Scaling behavior |
|---|---|---|
Medusa API | Stateless | Scales horizontally; add instances freely |
PostgreSQL | Stateful | Vertical scaling, then read replicas |
Redis | Stateful | Shared across all API instances |
Storefront | Stateless | Scales independently, often statically |
Admin dashboard | Served by the API | Scales with the API |
The API layer is the easy part. Because Medusa's API instances hold no session state locally, any instance can serve any request. Add instances, put them behind a load balancer, done.
The database is where capacity actually binds. You can run ten API instances, but they all query one Postgres. Connection pool exhaustion arrives long before CPU saturation on a well-provisioned database, and it's the constraint most teams meet first.
Redis is shared infrastructure, not a per-instance cache. Medusa uses it for event queuing, background jobs, and session storage. If each instance had its own Redis, jobs would be processed inconsistently and sessions wouldn't survive a load balancer routing a customer to a different instance mid-checkout.
The storefront is entirely separate. This is the point of headless: your Next.js frontend deploys on its own schedule, and swapping it for something else doesn't touch commerce logic.
What You Need in Place
A Medusa project in a Git repository. Either the official starter or your own build. GitHub and Bitbucket both connect.
PostgreSQL. Medusa stores everything persistent here — products, orders, customers, regions, price lists. This is business-critical data, so backup configuration matters from day one rather than as a later task. Managed Postgres handles provisioning, backups, and failover.
Redis for event queuing, background jobs, and session storage. Technically optional for a minimal setup; practically necessary for anything with real traffic, because without it background job processing and session handling both degrade under concurrency.
A current Node.js runtime. Medusa tracks active LTS releases. Check Medusa's own documentation for the supported range rather than assuming — it moves with each major release.
Environment configuration for database connection, Redis connection, JWT secret, cookie secret, and CORS origins. More on CORS below, because it deserves its own attention.
A note on Medusa versions
Medusa's CLI commands, configuration file format, and module structure changed meaningfully between major versions. Migration commands, seeding, and admin user creation all have different syntax depending on which release you're running.
Rather than reproduce commands that may not match your version, check Medusa's current documentation for CLI syntax. The deployment concepts below apply across versions; the exact commands don't.
Preparing the Repository
Read configuration from the environment, never from hardcoded values. Your Medusa config should pull database URLs, Redis URLs, secrets, and CORS origins from environment variables. This is the single most impactful thing you can do to avoid deployment failures, because it lets the same tested artifact run in development, preview, and production with only configuration differing.
Define build and start scripts explicitly in package.json. Medusa's build step compiles TypeScript and generates the admin dashboard, so a deployment that skips it produces a container that starts and then fails in confusing ways.
Commit an environment template listing every required variable without values. This documents what the application needs and makes configuration review possible during onboarding or incident response.
Verify locally before pushing. Run the build, run the start command, confirm the API responds. A build failure on your machine costs two minutes; the same failure in a cloud pipeline costs a build cycle and a log-reading session.
Monorepo considerations
If you're hosting the storefront and backend in one repository, they're still two deployable services with different build commands, different runtimes, and different scaling profiles. The storefront is often mostly static and scales cheaply; the backend is a stateful-dependency-having API. Treating them as one deployable unit couples release cycles that have no reason to be coupled.
Database and Redis Setup
Colocate database and application. Every API request generates multiple database queries, and cross-region round trips add latency to each one. Application and database in the same region is the single easiest latency win available, and it costs nothing.
Size the database for connections, not just CPU. Each API instance maintains a connection pool. Ten instances with a pool of ten each is a hundred connections before any traffic arrives. Postgres has a connection limit, and exceeding it produces errors that look like application bugs. Monitor pool utilization and scale before you approach the ceiling.
Configure automated backups immediately. Order data is not reconstructible. A store that loses a day of orders has lost the orders, the fulfillment obligations, and the customer relationships attached to them. Backup retention is a business decision, not a technical default.
Store connection strings as environment variables, never in the repository. See environment variables for how configuration is injected at runtime. Anything committed to Git is compromised permanently, even after removal — Git history is designed not to forget.
Migrations
Run migrations before new containers serve traffic, not after. Deploying new application code against an old schema produces errors at best and inconsistent writes at worst. The ordering is:
Run migrations against the database
Start new containers
Route traffic to them
Retire old containers
This ordering matters most during zero-downtime deployments, where old and new containers briefly run simultaneously. If a migration is backward-incompatible, the old containers break the moment it runs. For schema changes that can't be made backward-compatible, plan a maintenance window rather than discovering the problem live.
Configuration That Breaks Deployments
CORS
Medusa requires explicit CORS configuration for its storefront, admin, and auth endpoints separately. Misconfigured CORS is probably the most common cause of a Medusa deployment that builds successfully, starts successfully, passes health checks, and then doesn't work.
The symptom is distinctive: the API responds fine to direct requests, but the frontend can't reach it. Browser console shows CORS errors. Everything looks healthy from the server side, which is why teams spend hours checking the wrong layer.
Set each origin to your exact domains — including protocol, and including preview or staging domains if you use them. A wildcard in production is a security problem, not a shortcut.
Secrets
JWT and cookie secrets sign session tokens. They need to be genuinely random, different between environments, and rotated on a schedule. If they leak, session forgery becomes possible.
Keep them in environment configuration, never in the repository. Rotating them invalidates existing sessions, so plan rotation for low-traffic periods.
Health and readiness
Configure your health check against an endpoint that reflects actual serviceability — one that confirms the application can reach its database, not merely that the Node process started.
A container that's running but can't reach Postgres will pass a naive health check and then fail every request. Distinguishing "process alive" from "can actually serve" is what prevents traffic routing to containers that can't handle it. The same pattern appears in troubleshooting deployment issues — it's a general deployment failure mode, not a Medusa-specific one.
Deployment and Scaling
Connect the repository and enable automatic builds. Git-based deployment with auto-deploy means changes ship through a reviewable commit rather than a manual process. For commerce, that audit trail matters — you want to know what changed before a checkout bug appeared.
Use zero-downtime deployment. New containers start and pass health checks before old ones retire. For a store, even brief unavailability means abandoned carts, and the customer doesn't come back to explain what happened.
Set scaling bounds. Scaling presets define minimum and maximum instances. A minimum above one gives you redundancy — a single instance is a single point of failure, and commerce traffic doesn't wait for a restart. A maximum protects against runaway scaling from a traffic anomaly or retry storm.
Scale on signals that reflect real load. CPU utilization works reasonably for Node.js API workloads. Request queue depth and response latency are often better leading indicators, since they degrade before CPU saturates.
Remember the database doesn't scale with the API. Adding instances multiplies database connections. Past a certain point, more API instances make things worse rather than better by exhausting the connection pool. When you hit that wall, the answer is database capacity or read replicas, not more application containers.
Attach a custom domain with TLS — see custom domains.
Keep rollback available. Logs and rollbacks let you revert to the previous version while preserving the failed deploy's output for diagnosis. Note that rollback reverts application code, not database schema — if a deployment included a migration, reverting the code may leave you running old code against a new schema. Plan migrations with that asymmetry in mind.
Cloud deployment vs. traditional hosting covers the broader workflow comparison.
Monitoring a Production Store
Commerce has failure modes that generic application monitoring misses.
Layer | What to track | Why |
|---|---|---|
Infrastructure | CPU, memory, instance count | Capacity and scaling behavior |
Application | Response latency p50/p95/p99, error rate by endpoint | Where degradation is happening |
Database | Connection pool usage, query latency, slow queries | Usually the real bottleneck |
Redis | Memory usage, queue depth, eviction rate | Background job health |
Business | Checkout completion rate, cart creation, order volume | Catches failures metrics miss |
That last row is the one that matters most and is most often absent. A checkout that fails for a subset of customers — one payment provider, one region, one shipping option — may not move your error rate detectably. But order volume dropping against its normal pattern tells you immediately, and it's the only signal that catches a functional break where every technical metric stays green.
Alert on p95 and p99, not averages. Averages hide the tail, and the tail is the customer who gave up.
Watch database connection pool utilization specifically. It's the metric that predicts the failure mode most likely to take down a scaled Medusa deployment, and it's rarely on a default dashboard.
Wire container metrics and custom application metrics through metric ingestion so infrastructure and business signals appear together rather than in separate tools you correlate by timestamp.
Security Considerations
Payment data should never touch your servers. Medusa integrates with established payment providers, which means card data goes directly from the customer's browser to the provider. This is the architecture that keeps your compliance scope small — your servers handle payment references, not payment instruments. Any design where card data passes through your application dramatically expands what you're responsible for.
Restrict admin access. The admin dashboard can modify pricing, inventory, orders, and customer records. Use role-based permissions to limit what each account can do, and keep admin accounts to the minimum set of people who need them.
Rotate secrets on a schedule. JWT secrets, cookie secrets, database credentials, and API keys all benefit from rotation. See API key management.
Keep dependencies current. A Node.js commerce application has a substantial dependency tree. Automated vulnerability scanning in your pipeline catches known issues before they reach production.
On compliance: headless architecture and managed infrastructure both help with compliance posture, but neither delivers it on its own. Your obligations depend on where you operate, what data you handle, and how you handle it — and they involve process and documentation, not just technology. For platform-level details, see the security overview and trust center. For your own obligations, talk to someone qualified to advise on them.
Extending a Headless Store
Decoupling the frontend from commerce logic opens up integrations that are awkward on monolithic platforms.
Semantic product search. Keyword search misses intent — a customer searching "something warm for hiking" gets nothing useful from exact matching. Vector search over product descriptions matches meaning rather than words.
Generated product copy. Descriptions, metadata, and category summaries at catalog scale, using the inference API. Useful when importing thousands of SKUs with thin supplier data.
Support automation. Order status, returns policy, and shipping questions answered against your own documentation and order data.
Multiple storefronts on one backend. Web, mobile, in-store kiosk, marketplace integration — all consuming the same commerce API. This is headless architecture's clearest structural advantage, and it costs nothing extra to preserve as an option.
Running these on the same platform as your store means one console, one identity model, and one place to look when something breaks — rather than correlating across vendors during an incident.
Common Mistakes
Hardcoding secrets in the repository. Git history is permanent. A committed credential is compromised even after you remove it, and must be rotated rather than deleted.
Running migrations after deployment. New code against an old schema produces errors and potentially inconsistent writes. Migrate first, then start new containers.
Misconfiguring CORS. Builds succeed, health checks pass, and the storefront can't reach the API. Set storefront, admin, and auth origins explicitly to exact domains.
Under-provisioning the database. The most common real bottleneck. Watch connection pool utilization, not just CPU, and scale the database before adding more API instances.
Running a single instance in production. One instance means every restart, deploy, and crash is downtime. For commerce, downtime is abandoned carts.
No business-level monitoring. Technical metrics can look perfect while checkout is broken for a subset of customers. Track order volume against its expected pattern.
Coupling storefront and backend releases. If a copy change on the storefront requires a backend deploy, you've rebuilt a monolith with extra steps.
Assuming rollback undoes migrations. Reverting code doesn't revert schema. Design migrations to be backward-compatible where possible, so a rollback doesn't leave old code facing a new schema.
Frequently Asked Questions
What is Medusa and why use it for headless commerce?
Medusa is an open-source commerce engine built on Node.js, providing modular APIs for products, orders, customers, payments, and fulfillment. Teams choose it for customization depth, horizontal scalability, and data ownership — you run it on your own infrastructure and can modify any part of it. The trade-off against hosted SaaS commerce is that you take on operational responsibility in exchange for control.
What database does Medusa require?
PostgreSQL, for all persistent data — products, orders, customers, regions, and sessions. Check Medusa's documentation for the minimum supported version, which advances with major releases. Redis is strongly recommended alongside it for event queuing, background jobs, and session storage; without it, background processing and session handling degrade under concurrency.
Can I deploy an existing Medusa project?
Yes. Connect the repository, configure environment variables, and deploy. The main requirement is that build and start scripts are defined and that configuration reads from environment variables rather than hardcoded values. Projects that hardcode configuration need that changed first, since it's what allows one artifact to run across environments.
How does headless commerce differ from a traditional platform?
Traditional platforms couple the storefront to the commerce backend — changing the shopping experience means working within the platform's templating system. Headless separates them: the backend exposes an API, and any number of frontends consume it. You gain frontend freedom and the ability to serve multiple channels from one backend, and you take on responsibility for building and hosting the frontend yourself.
How do I handle traffic spikes during sales events?
Medusa's stateless API scales horizontally, so additional instances handle additional traffic. The constraint is usually the database, since every API instance shares it. Before a known spike, verify database capacity and connection pool headroom, raise your scaling ceiling, and load test against a production-like configuration. Scaling the API without scaling the database moves the bottleneck rather than removing it.
What happens if a deployment fails?
Health checks prevent traffic routing to containers that fail them, so a broken build shouldn't reach customers. If a deployment starts but behaves badly, roll back to the previous version and diagnose from the retained logs. The caveat is schema: if the deployment included a migration, reverting code may leave old code running against a new schema. Design migrations to be backward-compatible where you can.
Is a headless store secure for payment processing?
The architecture helps, because integrating with established payment providers means card data goes from the customer's browser directly to the provider and never touches your servers. That keeps your compliance scope narrow. It doesn't remove your responsibilities around access control, secret management, dependency hygiene, and the data you do store — and your specific obligations depend on your jurisdiction and business model.
How do I know when to scale the database instead of the API?
Watch connection pool utilization and query latency. If API instances are adding up to a high connection count while individual query times climb, adding more instances will make things worse — each new one claims more connections from a pool that's already strained. That's the signal to scale the database vertically or introduce read replicas.
Getting Started
The deployment path for Medusa is straightforward once the dependencies are in place: repository connected with automatic builds, Postgres and Redis provisioned in the same region as the application, environment configuration set with CORS pointing at your exact domains, migrations running before new containers take traffic, and scaling bounds with a minimum above one.
The parts worth attention aren't the deployment mechanics — they're the database as your real capacity constraint, CORS as the configuration most likely to produce a silent failure, and business metrics as the monitoring layer that catches what technical metrics miss.
Start with a small configuration, load test against something resembling production, and scale on evidence rather than estimate.