- One public Shopify app and shared Hydrogen codebase can support many merchants without storefront forks.
- Dedicated tenant subdomains and Hydrogen processes provide runtime isolation while preserving centralized delivery.
- OAuth, billing, Shopify Admin access, and authoritative business logic belong in the shared application layer.
- Provisioning and deprovisioning must manage credentials, runtime processes, routing, database state, and cleanup together.
- Operational maturity depends on secret management, monitoring, wildcard TLS, and a plan for horizontal scaling.
Building a Multi-Tenant Shopify POS Product Without Forking the Storefront
Multi-tenant commerce products often face an uncomfortable choice: build a separate storefront for every merchant, or force every merchant into one rigid experience. Rapora was designed to avoid that tradeoff.
The platform combines a publicly installable Shopify app, Shopify POS extensions, and a shared Shopify Hydrogen storefront codebase. Each merchant gets an isolated storefront runtime and a dedicated subdomain, while the application logic, deployment pipeline, and operational controls stay centralized. The result is a repeatable foundation for consultative retail workflows such as guided selling, sales-representative experiences, quotes, and customer-assisted checkout.
This approach illustrates a broader principle in ecommerce development: merchant-specific behavior does not always require merchant-specific codebases. With well-defined tenant boundaries, a shared platform can deliver tailored experiences without multiplying maintenance work.
The Challenge: Tailored Merchant Experiences Without Codebase Sprawl
Retail businesses need flexibility. One merchant may need particular sales-representative defaults, billable locations, checkout options, or storefront behavior, while another needs a different configuration. The conventional response is often to fork the storefront for each merchant.
Forking seems expedient at first, but it creates compounding costs:
- Every feature, security update, and dependency upgrade must be repeated or merged across storefront repositories.
- Release confidence drops as merchants drift onto different code versions.
- Bug fixes become harder to validate because production behavior differs by branch.
- Provisioning a new merchant becomes a custom engineering exercise rather than an operating procedure.
Rapora instead uses a shared-build, isolated-runtime model. The platform preserves a common storefront implementation while giving each merchant a dedicated Hydrogen process, tenant configuration, credentials, and subdomain.
The Core Architecture: Shared Control Plane, Isolated Storefront Runtimes
The architecture separates responsibilities based on where trust, business authority, and customer experience belong.
Layer | Primary responsibility | Why it belongs there |
|---|---|---|
Shared POS application | OAuth, Shopify Admin API access, billing, webhooks, provisioning, persistence, and authoritative business rules | These functions need centralized control, durable records, and strong authentication. |
Hydrogen storefront | Catalog presentation, customer-facing cart flows, checkout UX, and storefront interactions | This layer is optimized for the merchant-facing and shopper-facing web experience. |
Shopify POS extension | In-store workflows for sales representatives | The extension operates in Shopify POS and can use Shopify session tokens for authenticated requests. |
Tenant runtime | A dedicated Hydrogen process and tenant subdomain | Per-merchant execution and configuration create a practical isolation boundary without a storefront fork. |
The shared application, referred to as pos-app, runs as a centralized process. It exposes APIs for POS and Hydrogen use cases. It is the system of record for tenant metadata and operational workflows.
The storefront is built once from the shared Hydrogen codebase, then run once per merchant. A merchant named northside, for example, receives a PM2 process named hydrogen-northside and can be served from a corresponding tenant subdomain. This creates separation where it matters operationally while keeping feature development on one code line.
flowchart LR merchant["Merchant installs public Shopify app"] --> onboarding["Onboarding and billing"] onboarding --> pos["Shared pos-app: OAuth, Admin APIs, tenant records"] pos --> provision["Provision tenant credentials, environment, routing, runtime"] provision --> hydrogen["Dedicated Hydrogen process and subdomain"] pos --> posExtension["Shopify POS extension"] hydrogen --> storefront["Shopper storefront, cart, and checkout UX"]
Why the Separation of Responsibilities Matters
In a multi-tenant system, the question is not simply whether components can communicate. The more important question is which component should be trusted to perform a given action.
Rapora keeps Shopify Admin concerns and business-critical operations in pos-app. This includes Shopify OAuth, billing state, webhook handling, tenant persistence, and provisioning. The application also owns the authoritative rules behind consultative retail workflows. Centralizing those functions reduces duplicated integrations and gives the platform one place to enforce tenant-aware access controls.
Hydrogen focuses on presentation and commerce UX. It retrieves catalog and cart information through Shopify’s Storefront API, while protected business information is obtained through server-to-server calls to the shared application. This pattern avoids exposing long-lived application credentials in a shopper’s browser.
That distinction is especially valuable for headless commerce teams. A storefront should be fast and flexible, but it should not become a hidden repository for administrative credentials or backend business rules. For teams evaluating this architecture, Shopify Hydrogen development offers a useful way to combine React-based storefront control with Shopify-native commerce capabilities.
Tenant Authentication: Scoped Secrets Instead of Browser-Exposed Credentials
The Hydrogen storefront communicates with pos-app through authenticated server-to-server requests. It sends an Authorization: Bearer header, and the shared application validates the supplied secret against the tenant’s hydrogenApiSecret in Postgres.
That tenant-specific validation is important. It ensures a storefront runtime is associated with the correct shop rather than relying solely on a universal secret shared across every merchant. A global secret can remain temporarily as a migration fallback, but the steady-state model is per-shop credentials.
The POS extension follows a separate, platform-appropriate pattern: it uses Shopify session tokens to call authenticated pos-app routes. In other words, the trust model matches the calling environment:
- POS extension: Shopify-issued session tokens authenticate the in-POS user and shop context.
- Hydrogen server: tenant-scoped secrets authenticate protected server-to-server requests.
- Browser: no long-lived backend secret is exposed to the shopper.
This is a practical example of treating APIs as products with explicit consumers, identities, and boundaries. It also aligns with the kind of permissions, workflow modeling, and integration discipline used in database-driven web applications.
Provisioning Is a Product Workflow, Not a Collection of Server Commands
The merchant experience begins with app installation and onboarding. A merchant selects or supplies the information needed to operate their retail experience: a subdomain, sales representative, checkout method, billable locations, and plan. The system persists the onboarding state before billing so it can resume automatically after billing approval.
Once the merchant is ready, provisioning creates and reconciles the technical resources required for a live tenant:
- Mint tenant Storefront API credentials.
- Write tenant configuration, including
hydrogen.env, under/etc/<APP_NAME>/tenants/<slug>/. - Create or reconcile the merchant’s PM2 runtime, named
hydrogen-<slug>. - Write or update the tenant’s Nginx routing configuration.
- Persist the resulting tenant state in Postgres.
- Mark the shop active only after the environment is successfully configured.
Each step is an explicit boundary between application state and infrastructure state. That matters because a merchant should never appear active in the database if the corresponding runtime cannot serve requests. Conversely, a server process should not remain running indefinitely after the merchant is removed from the application.
A representative tenant configuration might resemble the following:
Illustrative environment values for one tenant runtime.
TENANT_SLUG=northside
SHOPIFY_STORE_DOMAIN=northside.myshopify.com
STOREFRONT_API_TOKEN=tenant-specific-token
POS_API_BASE_URL=https://app.rapora.shop
POS_API_SECRET=tenant-specific-secretRouting One Platform to Many Merchant Storefronts
Nginx provides the routing boundary between the shared application and tenant storefronts. The main application host, such as app.rapora.shop, routes to pos-app. Tenant hosts matching *.app.rapora.shop route to their respective Hydrogen processes through per-tenant configuration.
Wildcard DNS and TLS are foundational to this pattern. They allow new merchant subdomains to be provisioned predictably without asking every tenant to manage a separate domain configuration. At the same time, wildcard certificates, host-routing rules, and tenant records must be kept in sync.
Common production failures tend to occur at these boundaries:
- The application host is incorrectly routed to a tenant storefront, or a tenant hostname is sent to the shared application.
- A Storefront API scope or token is stale, missing, or mismatched.
- A tenant environment file was not written or cannot be read by the runtime.
- A PM2 process remains after a tenant is removed, creating an orphaned runtime.
The lesson is simple: multi-tenancy is not achieved by a database table alone. It also requires disciplined management of hosts, credentials, process state, and observability. These are core concerns for managed hosting and application operations, particularly when releases and runtime configuration must remain dependable as the tenant count grows.
Deployment: Build Once, Reconcile Many
GitLab CI delivers the shared pos-app and Hydrogen artifacts, runs Prisma migrations, reloads application processes, and reconciles tenant runtimes on the production server. The important distinction is between delivering a common build and ensuring each tenant runtime is correctly configured to use it.
This deployment model avoids treating every merchant as an independent application release. Instead, the pipeline updates shared code once and then verifies that each tenant process, environment file, and routing definition is present and healthy.
That reconciliation mindset is more resilient than relying on manual server changes. It enables deployments to repair drift, recreate missing processes, and surface exceptions in a consistent place. It also gives teams a clearer operating model: shared application artifacts are deployed globally, while tenant runtime state is reconciled from authoritative tenant records.
Deprovisioning and Privacy Workflows Must Be First-Class
Tenant lifecycle management does not end at onboarding. Shopify uninstall events and GDPR shop-redaction flows need to reverse the same operational decisions made during provisioning.
When a merchant is deprovisioned, the platform removes the tenant runtime and Nginx state, deletes tenant secrets, and purges applicable operational data. Treating this as a formal workflow reduces security exposure, prevents unnecessary server resource use, and helps ensure retained data matches the platform’s privacy obligations.
This symmetry is an important design standard:
- Provisioning creates database state, credentials, configuration, routes, and processes.
- Deprovisioning removes or invalidates those same resources in a controlled order.
When onboarding and offboarding are designed together, the system is easier to audit, easier to support, and less likely to accumulate configuration debt.
Capacity Planning: Isolation Has a Real Resource Cost
Dedicated tenant processes provide operational isolation, but they consume memory and CPU. In the documented baseline, a smaller server with 2 vCPUs and 4 GB of memory supports roughly 1–5 tenants, while a 4-vCPU, 8-GB server or larger can support roughly 5–20 tenants. Each tenant runtime may add approximately 150–400 MB depending on traffic and workload.
These figures are planning baselines rather than permanent guarantees. Traffic patterns, process configuration, cache behavior, background work, and tenant-specific complexity all affect actual capacity. The right response is to establish metrics early: per-process memory, CPU, restart frequency, request latency, error rates, and host-level saturation.
What Comes Next: From a Strong Baseline to a Scalable Platform
The single-VM architecture is a practical starting point for a production-oriented retail product. It gives each merchant a distinct runtime while keeping deployment and support manageable. However, its current socket-routing approach also defines the boundary for future scale.
Several next steps can strengthen the platform as adoption grows:
- Asynchronous provisioning: move longer infrastructure work out of the request path and make progress observable and retryable.
- Encryption at rest: protect tenant secrets beyond application-level access controls.
- External secret storage: reduce dependence on server-local environment files as the infrastructure expands.
- Automated wildcard TLS: make certificate issuance and renewal more hands-off and verifiable.
- Customer Account API support: extend tenant-specific customer experiences where appropriate.
- Horizontal scaling: evolve routing so tenant traffic can reach healthy runtimes across multiple nodes rather than one host.
These are not signs that the original approach is incomplete. They are the natural next concerns once a shared platform proves its value and must serve more merchants with stronger resilience requirements.
The Key Takeaway
Rapora demonstrates that a multi-tenant Shopify product can offer merchant-specific storefronts and POS workflows without maintaining a collection of storefront forks. The enabling idea is not merely “shared code.” It is a deliberate division of responsibility: centralized control for OAuth, billing, data, and business rules; isolated runtimes for each merchant’s storefront; and automation that keeps database, deployment, routing, process, and secret state aligned.
For consultative retailers, this creates a repeatable path from app installation to a live, tenant-specific selling experience. For engineering teams, it turns a potentially unmanageable fleet of merchant storefronts into one platform with explicit boundaries, predictable operations, and a clear roadmap to scale. Organizations planning similar products can benefit from early technology strategy and architecture planning before tenant-specific requirements become permanent codebase divergence.
