A corporate client was paying $1,500/month in licensing fees to a low-code agency for a white-label enterprise platform they didn't own, couldn't modify, and couldn't migrate away from. I rebuilt the entire system as a self-hosted Next.js application on Railway, implemented dynamic Tailwind theme switching for their white-label clients, and transferred 100% IP ownership — dropping their recurring infrastructure cost to $14/month.
The Vendor Lock-In Trap
The client's situation was a textbook case of platform dependency. A boutique agency had built their enterprise portal on a proprietary low-code platform (Bubble.io) and charged a monthly "platform licensing fee" on top of the initial development cost.
The consequences compounded over three years:
- $54,000 in cumulative licensing fees paid for a platform the client had no ownership stake in.
- Zero source code access. The client could not view, modify, or export the application logic. Every change request went through the agency at $150/hour.
- Feature ceiling. Bubble's visual programming model could not support the client's requirement for custom PDF report generation, background job scheduling, or advanced role hierarchies.
- Migration threat as leverage. When the client asked about reducing the monthly fee, the agency reminded them that migrating away would mean starting from scratch — because no exportable codebase existed.
The client wasn't paying for software. They were paying rent on someone else's code running on someone else's infrastructure.
The Migration Architecture
The rebuild replaced every layer of the original system with open infrastructure the client fully owns:
| Layer | Old (Bubble + Agency) | New (Next.js + Railway) | |---|---|---| | Frontend | Bubble visual editor | Next.js App Router + Tailwind CSS | | Database | Bubble internal DB (no export) | PostgreSQL on Railway | | Authentication | Bubble auth (proprietary) | Supabase Auth (open source) | | File Storage | Bubble file uploads | Supabase Storage (S3-compatible) | | Hosting | Bubble cloud (mandatory) | Railway Docker container | | Source Code | Agency-owned, inaccessible | Client-owned GitHub repository |
Dynamic Theme Switching for White-Label Clients
The platform serves multiple corporate clients, each requiring their own branding: logo, primary color palette, typography, and email templates. Instead of maintaining separate codebases or CSS bundles per client, the system uses CSS custom properties resolved at runtime.
// lib/theme.ts — Type-safe tenant theme configuration
interface TenantTheme {
brandPrimary: string;
brandSecondary: string;
brandAccent: string;
logoUrl: string;
fontFamily: string;
borderRadius: string;
}
const TENANT_THEMES: Record<string, TenantTheme> = {
'acme-corp': {
brandPrimary: '#1e40af',
brandSecondary: '#3b82f6',
brandAccent: '#f59e0b',
logoUrl: '/tenants/acme-corp/logo.svg',
fontFamily: '"Inter", sans-serif',
borderRadius: '0.75rem',
},
'nova-industries': {
brandPrimary: '#059669',
brandSecondary: '#10b981',
brandAccent: '#8b5cf6',
logoUrl: '/tenants/nova-industries/logo.svg',
fontFamily: '"Outfit", sans-serif',
borderRadius: '0.5rem',
},
};
export function getThemeForTenant(tenantSlug: string): TenantTheme {
return TENANT_THEMES[tenantSlug] ?? TENANT_THEMES['acme-corp'];
}
// app/layout.tsx — Inject CSS variables at the root
// These variables are consumed by Tailwind utilities like
// bg-[var(--brand-primary)] and text-[var(--brand-secondary)]
export function generateThemeStyles(theme: TenantTheme): string {
return `
:root {
--brand-primary: ${theme.brandPrimary};
--brand-secondary: ${theme.brandSecondary};
--brand-accent: ${theme.brandAccent};
--font-family: ${theme.fontFamily};
--radius: ${theme.borderRadius};
}
`;
}
The root layout reads the tenant slug from the subdomain (via middleware) and injects the corresponding CSS variables into a <style> tag. Every Tailwind utility class referencing var(--brand-*) automatically resolves to the correct tenant's palette. No conditional CSS imports. No client-side theme context. No flash of unstyled content.

Runtime Environment Configuration
Each white-label tenant may require different feature flags, API endpoints, and third-party integrations. Instead of environment variables (which are baked into the build), the system loads tenant configuration from the database at request time:
A Server Component at the layout level fetches the tenant's configuration row from PostgreSQL, which includes feature flags (enablePdfExports, enableAdvancedAnalytics, maxUserSeats), integration keys, and billing tier. This configuration propagates through the component tree via React server-side props — no client-side fetching, no loading states.
Background Job Infrastructure
The original Bubble platform had no concept of background processing. The rebuilt system runs long-running jobs (PDF generation, data aggregation, scheduled email reports) as separate Railway services within the same private network:
- A worker service polls a PostgreSQL job queue table for pending tasks.
- Jobs are processed in isolated Docker containers with configurable resource limits.
- Completed results are written back to the database and trigger a Postgres
NOTIFYevent to update any connected dashboard in real time.
This architecture scales horizontally. Adding a second worker service doubles processing throughput with zero code changes.
Cost Comparison
| Line Item | Bubble + Agency | Next.js + Railway | |---|---|---| | Platform Licensing | $1,500/month | $0 | | Hosting | Included in license | $8/month (Railway) | | Database | Included (no export) | $6/month (Railway PostgreSQL) | | Change Requests | $150/hour (agency) | $0 (self-modifiable) | | Source Code Ownership | Agency-owned | 100% client-owned | | Total Monthly Cost | $1,500+ | $14 | | Annual Savings | — | $17,832 |

What the Client Now Owns
The client has the GitHub repository with full commit history, the Railway project with database and worker services, the Supabase Auth instance, and complete documentation of the deployment pipeline. They can hire any developer to modify, extend, or migrate the platform.
No licensing fees. No agency dependency. No export restrictions. The code is theirs.
Skip the technical debt. Let a senior engineer build your core architecture from scratch. View my custom Next.js engineering tiers on Fiverr.