In 1844, Samuel Morse sent the first telegraph message over long distance: “What hath God wrought.”
While primitive by today’s standards, the telegraph was a monumental invention. It revolutionized communication - messages that once took days by horse could travel instantly across continents.
Yet this marvel came with a hidden cost: dependency on telegraph companies. To send a message, you needed their infrastructure, their operators, their pricing. You could write any message you wanted, but only they could transmit it. The cost was not cheap; a transatlantic message in 1866 cost ten dollars a word, with a ten word minimum - meaning that a simple message would cost $100. That’s expensive in today’s currency, but not unaffordable for important business messages; however, if you adjust for inflation, that same message would cost just over $2,000 today.
The telegraph was technologically impressive, true - but the cost was a significant factor for a very long time.
Modern backend-as-a-service platforms present a somewhat similar trade-off; they are fashionable among technologists, and while, for some uses cases, the cost may be reasonable, for many uses, scaling can be quite expensive. Further, whether intentional or not, migrating between them can be difficult. Vendor lock-in is a real concern - not merely because of what the company might do in the short term, but because of what they or a future corporate owner might do in the medium to long term.
AI-based platforms have other concerns; like telegraphs, they are a new technology with interesting possibilities. Somewhat unlike telegraphs, though, AI code generation has caused serious problems almost immediately: security breaches, unmaintainable code, and so forth. Requests for traditional programming techniques to migrate from such platforms have become common - I believe it’s reasonable to say the consensus opinion among software development professionals is that while non-technical stakeholders may make a proof-of-concept with AI tools, you should have a web developer review and/or re-implement your app before production.
In this article, we will be discussing one particular AI backend platform: Base44. Base44 lets you describe an application in natural language and generates a complete full-stack application - database, authentication, serverless functions, hosting, everything. Like the telegraph, it represents a significant advancement for certain use cases. You can build a working application in hours that might otherwise take weeks.
But there’s still a vendor dependency: while Base44 allows code export on all plans via ZIP download (with direct GitHub export requiring Builder at $50/month or higher), the exported code remains tethered to Base44’s backend services through their SDK. You own the frontend code; the backend - database operations, authentication, business logic - continues running exclusively on Base44’s servers. Even if you deploy the frontend elsewhere, every data operation still requires a call to Base44’s API.
This architectural constraint often reveals itself gradually. Perhaps you need custom database indexes Base44 doesn’t support. Maybe you require complex transaction handling that doesn’t fit their NoSQL backend. Or you want to escape the monthly fees as your application scales. Whatever the reason, if you’ve built something on Base44 and need independence from the platform, migration becomes necessary.
In this article, this guide walks through planning a migration from Base44’s platform and reimplementing it with Ruby on Rails 7.1 and Inertia.js. We’ll examine the technical architecture, assess migration complexity, and provide concrete patterns for preserving functionality while gaining complete control over your application stack. A future article will discuss more technical details of moving code from one platform to another.
Understanding What You’re Migrating From
Before planning any migration, we need to understand Base44’s architecture in detail. Base44 applications typically consist of several components that appear unified but are, technically speaking, quite separate.
The frontend is typically React, Vue, Svelte, or another modern JavaScript framework. Base44 generates this code based on your natural language description. The generated components handle routing, rendering, and user interaction. You can generally export this code and modify it, though it’s designed primarily to work with Base44’s SDK.
The backend runs as TypeScript serverless functions on Deno runtime - a secure JavaScript/TypeScript runtime built on V8 with Rust. These functions execute in isolated environments with explicit permission models, located in the base44/functions/ directory of your project. Each function handles specific operations: webhook processing, third-party API orchestration, scheduled tasks, or complex business logic that shouldn’t execute client-side. While you can export these TypeScript files, they depend heavily on Base44’s proprietary execution environment and cannot run standalone without significant refactoring.
The database uses a MongoDB-compatible engine storing data in Binary JSON (BSON) format. Base44 manages this database entirely - you define entity schemas in base44/entities/ using JSON Schema files (.jsonc), specifying allowed properties, data types, validation rules, and relationships. The platform handles indexing, replication, and query optimization behind the scenes. Data access occurs exclusively through the Base44 SDK’s entity methods like base44.entities.Task.list() or base44.entities.User.get(id), never through direct database connections. This abstraction enables features like row-level security but prevents custom query optimization or database-level operations.
The SDK (@base44/sdk) bridges your frontend and backend to Base44’s services. Every database query, every authentication check, every serverless function call goes through this SDK to Base44’s servers. Even if you export your code and deploy it elsewhere, this SDK remains the critical dependency that keeps you tied to Base44’s platform.
Authentication and authorization are usually handled by Base44’s built-in systems. User signup, login, password reset, OAuth integration, and permission management typically run through Base44’s services. While your application configuration specifies which users can access which resources, Base44 enforces these rules on their servers.
This architecture often makes rapid development possible - Base44 handles infrastructure complexity. It also creates what many developers experience as a lock-in problem. You don’t own the database directly. You don’t control the authentication system at a fundamental level. You typically can’t modify how entity operations work internally. For production applications requiring deep customization, this can become limiting.
Why Migrate to Rails and Inertia
Of course, not every Base44 application needs migration. Several factors typically motivate teams to migrate from Base44 to a self-hosted stack. Understanding these motivations helps determine whether migration makes sense for your particular situation.
Cost considerations: Base44’s pricing starts at $20/month billed annually for the Starter plan. Code export is available to all plans via ZIP download; direct GitHub export requires Builder ($50/month) or higher. For many simple applications, this pricing is quite reasonable. As usage grows - particularly with serverless function invocations and storage - costs naturally increase. A self-hosted Rails application has different economics: typically higher initial development cost but more predictable operational costs that don’t necessarily scale with usage in the same way. Neither model is inherently superior; they serve different needs.
Vendor lock-in concerns: Depending on a platform’s SDK for core functionality can create certain risks. Base44 could potentially change pricing, alter functionality, or shift product direction. While Wix’s acquisition suggests stability, acquisitions often bring product changes - though these changes might be positive or negative depending on your needs. Owning your stack generally eliminates this dependency - you control the code, the database, and the deployment. Of course, self-hosting brings its own dependencies on infrastructure providers, framework maintainers, and your team’s expertise - but at least you can mix-and-match vendors and get competitive quotes.
Customization requirements: Base44’s architecture constrains how you implement features. If you need custom database indexes, complex transaction handling, specialized authentication flows, or integration with systems Base44 doesn’t support, you’re limited. Rails provides complete control over these aspects. You can modify any part of the application to meet specific requirements.
Performance and scaling: Base44’s serverless architecture works well for many applications, though it has certain limitations. Cold start latency on serverless functions can sometimes impact user experience. Database query optimization is generally limited to what Base44 exposes through their API. For applications with demanding performance requirements or scaling patterns, self-hosted infrastructure often provides more control. That said, Base44’s infrastructure likely handles many common scaling scenarios better than a hastily configured self-hosted setup - though, as mentioned at the outset, the single-vendor dependency is a dealbreaker for many decisionmakers, since your price today for a given level of traffic may become something altogether different tomorrow.
Development workflow preferences: Base44 optimizes for AI-assisted development and rapid prototyping. Traditional development workflows - version control, code review, testing frameworks, CI/CD pipelines - require more mature tooling; there are very good reason why these things are standard practice, and eschewing them can lead to a rapid and uncomfortably intimate familiarity with the downsides of eschewing them.
Of course, these benefits come with costs. Migration requires significant development effort. You’re rebuilding working functionality rather than creating new features. Your team needs Rails expertise or time to develop it. Operational responsibility shifts from Base44 to your infrastructure team. These investments typically make sense when long-term control and flexibility outweigh the convenience of a managed platform.
Assessment and Planning
Thorough assessment precedes any successful migration. We need to understand exactly what the Base44 application does, how it does it, and what dependencies exist. This assessment phase, though it may seem tedious, often determines whether the migration succeeds or fails.
Export and examine your Base44 code. Export your project to examine its structure. Base44 lets you download a ZIP on any plan, and export directly to GitHub on Builder or higher. The exported code reveals how your application is structured, which components are generated, and where Base44 dependencies exist.
Review the exported code’s key directories:
entities/contains JSON Schema definitions for your data modelsfunctions/contains serverless TypeScript functionsapi/contains Base44 SDK client configuration- Frontend framework directories (React components, Vue components, etc.)
Document your data model thoroughly. The entities/ directory contains JSON Schema files defining each entity. These schemas specify fields, types, validation rules, and relationships. Convert these into documentation - a spreadsheet or markdown document listing each entity, its fields, data types, validation requirements, and relationships to other entities. This becomes your specification for Rails models.
For example, if you have a Task entity defined as:
{
"type": "object",
"properties": {
"title": { "type": "string", "maxLength": 200 },
"description": { "type": "string" },
"status": { "type": "string", "enum": ["pending", "in_progress", "completed"] },
"dueDate": { "type": "string", "format": "date" },
"assignedTo": { "type": "string" }
},
"required": ["title", "status"]
}Document this as: “Task entity with required title (string, max 200 chars) and status (enum: pending, in_progress, completed), optional description (string), dueDate (date), and assignedTo (references User).”
Catalog backend functions and business logic. Review the functions/ directory to understand what server-side logic exists. Base44 functions typically handle operations too sensitive for the client - payment processing, sending emails, data aggregation, scheduled tasks, webhook handling. Document each function’s purpose, inputs, outputs, and dependencies.
Extract sample data for testing. Base44’s database isn’t directly accessible via connection strings. The platform provides data export through its dashboard interface (CSV or JSON formats) or programmatically via the SDK. When exporting programmatically, you’ll encounter several constraints: pagination limits (typically 100 records per request), rate limiting, and the need to handle nested relationships manually. Let’s build a robust export script that handles these real-world complexities.
First, a naive version that will quickly hit limitations:
// export-data-simple.ts
import { createClient } from '@base44/sdk';
const base44 = createClient({
apiKey: process.env.BASE44_API_KEY,
});
// Simple export for small datasets
const tasks = await base44.entities.Task.list({ limit: 100 });
console.log(JSON.stringify(tasks, null, 2));This is easy enough; unfortunately, we’ll quickly hit Base44’s limits.
Here’s a more robust version:
// export-data.ts - Handles Base44 pagination limits (100 records/page)
import { createClient } from '@base44/sdk';
import * as fs from 'fs/promises'; // Use promises for async file ops
const base44 = createClient({
apiKey: process.env.BASE44_API_KEY,
});
async function exportEntity(entityName: string) {
const allRecords = [];
let cursor = null;
let hasMore = true;
let page = 0;
while (hasMore) {
page += 1;
const response = await base44.entities[entityName].list({
limit: 100,
cursor,
});
allRecords.push(...response.items);
cursor = response.nextCursor;
hasMore = !!cursor;
console.log(`Fetched page ${page}: ${response.items.length} ${entityName} records (total: ${allRecords.length})`);
// Rate limit respect - Base44 may throttle
await new Promise((resolve) => setTimeout(resolve, 100));
}
await fs.mkdir('data', { recursive: true });
await fs.writeFile(`data/${entityName}.json`, JSON.stringify(allRecords, null, 2));
console.log(`SUCCESS: Exported ${allRecords.length} ${entityName} records to data/${entityName}.json`);
}
async function exportAll() {
const entities = ['User', 'Task', 'Project', 'Comment']; // Dependency order: Users first
for (const entity of entities) {
try {
await exportEntity(entity);
} catch (error) {
console.error(`FAIL: Failed to export ${entity}:`, error.message);
}
}
}
exportAll().catch(console.error);Note the pagination handling and rate limiting to respect Base44 API constraints.
Run this with:
$ npx tsx export-data.ts
Fetched 100 User records...
Fetched 156 User records...
Exported 156 User records
Fetched 100 Task records...
Fetched 200 Task records...
Fetched 287 Task records...
Exported 287 Task records
...This data serves as test fixtures during Rails development and helps us validate that migration preserves all records correctly.
Identify external integrations. Base44 functions often integrate with external services - payment processors like Stripe, email services like SendGrid, storage services like AWS S3. List these integrations, noting how they’re used and what credentials or configuration they require. Rails will need similar integrations, though implementation differs.
Audit authentication mechanisms. Understanding Base44’s authentication is critical for migration. Base44 typically uses JSON Web Tokens (JWT) for stateless authentication, with tokens containing user metadata and cryptographic signatures. If your application uses password authentication, Base44 stores password hashes using either bcrypt (cost factor 10-12) or increasingly, Argon2id for superior resistance to GPU attacks. OAuth integrations are handled through Base44’s built-in providers. Document which authentication methods your application uses - this determines your Devise configuration strategy.
Choose your migration strategy. Three approaches exist:
Complete cutover builds the entire Rails application before switching users from Base44 to Rails. This concentrates risk but provides a clean break. Users experience one transition rather than incremental changes. This works better for smaller applications where you can validate complete functionality before launch.
Parallel operation with gradual migration runs both applications simultaneously, routing different features to different backends. New Rails functionality deploys alongside existing Base44 features. Users gradually transition from Base44 to Rails feature by feature. This reduces risk but increases operational complexity - managing two deployments, coordinating database state, and handling authentication across both systems.
Proxy-based migration keeps Base44 as the backend temporarily while rebuilding the frontend with Inertia.js calling both Base44 APIs (via the SDK) and new Rails endpoints. As Rails functionality becomes ready, you switch specific entity operations from Base44 to Rails. Eventually, all operations run through Rails, and you discontinue Base44. This works well when frontend modernization is itself a goal.
For most applications, parallel operation provides the best balance. You can validate Rails functionality with real users on low-risk features before migrating critical operations. The operational overhead is real but manageable with modern deployment tools.
Considering Migration Alternatives
Before we proceed with Rails setup, though, it’s worth acknowledging that Rails with Inertia.js isn’t the only migration target. Several alternatives exist, each with their own trade-offs:
Node.js with Express or Fastify might feel more natural if your team is already comfortable with TypeScript from Base44. You could even reuse some Base44 function code more directly. The ecosystem is familiar, and tools like Prisma provide excellent database abstractions. However, you’d need to rebuild many conveniences that Rails provides out of the box.
Django with Django REST Framework offers Python’s ecosystem and excellent admin interfaces. If your team has Python expertise or your application involves data science components, Django might be the better choice. The trade-off is generally a more complex frontend integration compared to Inertia.js.
Laravel with Inertia.js provides a very similar developer experience to Rails but in PHP. If your team knows PHP or you’re deploying to shared hosting that better supports PHP, Laravel is an excellent choice. The patterns in this guide translate almost directly to Laravel.
Next.js or Remix for a full-stack JavaScript solution could allow more code reuse from Base44. These frameworks handle both frontend and backend, though you’d need to add your own authentication, authorization, and database layers - essentially rebuilding much of what Base44 provided.
We’re recommend Rails with Inertia.js because it provides a mature, batteries-included framework that offers powerful backend features with extremely high quality end user experience via Svelte. However, it’s ultimately up to you; your circumstances might make another choice more appropriate.
Moving Forward
Migrating from Base44 to Ruby on Rails with Inertia.js transforms an AI-generated, platform-dependent application into a fully controllable, self-hosted solution. This migration requires substantial effort - understanding Base44’s architecture, reimplementing business logic in Rails, migrating data, and validating behavior - but provides complete ownership of your application.
The key to successful migration is methodical execution. Start with thorough assessment and planning. Build Rails infrastructure incrementally, validating each piece. Test thoroughly, particularly for complex business logic. Deploy carefully with rollback plans ready.
Base44 serves an important role in rapid prototyping and validation. For proof-of-concept applications and MVPs, the speed of AI-generated full-stack applications is remarkable. For production applications requiring customization, performance optimization, or long-term cost control, migrating to a traditional stack like Rails provides better sustainability.
If you’re considering this migration and need guidance on assessment, planning, or execution, or if you’ve encountered challenges during a migration project, get in touch. We specialize in helping organizations escape vendor lock-in while preserving the business logic their users depend on.
You may also like...
The Wonder of Rails, Inertia, and Svelte for Web Development
A practical guide to combining Ruby on Rails, Inertia.js, and Svelte to deliver rapid full-stack development and exceptional long-term maintainability.
Export your Asana Tasks as Plaintext
Learn how to export Asana project data to plain text YAML files for long-term accessibility, custom analysis, and freedom from vendor lock-in.
The Importance of Locking Gem Versions in Ruby Projects
Learn why locking gem versions is crucial for Ruby stability, and how to prevent dependency conflicts and deployment surprises across environments.

