Move Your App to Another Host
The code, the data and the files are yours. Here is how to take them elsewhere.
A Totalum project is a standard Next.js app with a SQLite database used through Drizzle ORM, sign-in with better-auth, and the built-in features (e-mail, AI, files, payments…) of the open totalum-sdk package. Nothing in it is locked to Totalum: you can run it on any host, keep or replace the built-in features, and keep developing it with any tool.
#What moves, and what to decide
| Part | Where it lives | What you do |
|---|---|---|
| Code | The project's repository | Download it or sync it to GitHub |
| Database (tables, rows, users, sessions) | A SQLite database per environment | Export it as SQL, import it where you run the app |
| Uploaded files | Totalum's file storage | Keep them there, or copy them to your own storage |
| Sign-in | better-auth, in your code and your database | Moves with them. Keep the same BETTER_AUTH_SECRET so everyone stays signed in |
Built-in features (totalum.* calls) | Totalum, through the project key | Keep using them with the key (billed in credits), or replace them |
Two common paths:
- Keep the database and built-in features on Totalum, host the app yourself. No code changes: the app reaches its database and features over HTTPS with the project key. See Run Locally & Self-Host.
- Move everything. Export the data and files, point the app at your own database, and replace the built-in features you use. The steps below.
The project key works only while the project exists. Deleting the project on Totalum stops every built-in feature that still uses it.
#1. Get the code
Source download is included on paid plans.
- Platform: the project's Code tab → Download project. The account owner's ZIP also contains
.env(development) and.env.prod(live). - GitHub: connect a repository;
developholds every version,mainthe published one. - API: Download Source Code, and the environment files with Get Project Env.
export API_KEY="tlm_sk_your_key" PROJECT_ID="my-app" BASE="https://builder-api.totalum.app/api/v1/vcaas"
URL=$(curl -sf -H "api-key: $API_KEY" "$BASE/projects/$PROJECT_ID/source-code" | jq -r '.data.downloadUrl')
mkdir my-app && curl -sf "$URL" | tar -xz -C my-app && cd my-app
curl -sf -H "api-key: $API_KEY" "$BASE/projects/$PROJECT_ID/env?env=live" -o .env.prod#2. Export the database
Each project has a live database (the published site) and a development one (the preview). A backup is a complete dump in standard SQLite SQL: the schema, every row, the sign-in tables (users, password hashes, sessions) and the table _migrations, which records the migrations already applied.
- Platform: the Database tab → Backups → create a backup (or take last night's, on paid plans) → Download.
- API: Create Backup with
{"env":"live"}, wait until List Backups shows itcomplete, then Download Backup.
curl -sf -X POST -H "api-key: $API_KEY" -H "Content-Type: application/json" \
-d '{"env":"live"}' "$BASE/projects/$PROJECT_ID/database/backups"
# when its status is "complete", take its id from the list and download it
curl -sf -H "api-key: $API_KEY" "$BASE/projects/$PROJECT_ID/database/backups?env=live" | jq '.data.backups[] | {id, kind, status}'
curl -sf -H "api-key: $API_KEY" "$BASE/projects/$PROJECT_ID/database/backups/<backupId>/download" | jq -r '.data.url' | xargs curl -sfo live.sqlA scheduled backup is gzip-compressed (.sql.gz): gunzip it first. To check it, load it into a local file: sqlite3 app.db < live.sql. Stop writes on the old site (pause or unpublish it) before the final export, so no row is lost in between.
#3. Copy the stored files
Uploaded files stay on Totalum and keep working while the project exists. To take them too, run this script from the project folder with the live .env; it downloads every file into files-export/, under the same keys your rows store:
// scripts/export-files.ts — run: npx tsx --env-file=.env.prod scripts/export-files.ts
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { totalum } from 'totalum-sdk';
let cursor: string | undefined;
do {
const page = await totalum.files.list({ limit: 100, ...(cursor ? { cursor } : {}) });
for (const file of page.items) {
const res = await fetch(await totalum.files.getSignedUrl(file.key, { download: true }));
const path = join('files-export', file.key);
await mkdir(dirname(path), { recursive: true });
await writeFile(path, Buffer.from(await res.arrayBuffer()));
}
cursor = page.nextCursor ?? undefined;
} while (cursor);Upload the folder to your storage (S3, R2, a disk…) with the same keys, then replace the app's totalum.files calls with your storage's.
#4. Run it on another host
Use Node.js 22. Every option starts with npm ci.
#Your own Cloudflare account (closest to Totalum)
The app already runs on Cloudflare Workers. Inside a Worker, the database code uses the D1 database bound as TOTALUM_DB, so no code changes:
npx wrangler login
npx wrangler d1 create my-app-db # note the database_id it prints
npx wrangler d1 execute my-app-db --remote --file=live.sqlIn wrangler.jsonc, set name to your Worker's name and, in d1_databases, database_name and database_id to the new database (keep "binding": "TOTALUM_DB"). Set the secrets (see environment variables), then build and deploy:
npx wrangler secret put BETTER_AUTH_SECRET
npx opennextjs-cloudflare build
npx wrangler deployAttach your domain to the Worker in the Cloudflare dashboard (Workers → your Worker → Domains & Routes).
#A Node.js server, Vercel or any Next.js host
npm run build and npm start run the app on Node (port 3000). Outside a Worker the database code talks to Totalum over HTTPS with the project key — the self-host path. To use your own database instead, replace src/db/index.ts with Drizzle's libSQL driver, which reads a local SQLite file or a Turso database (@libsql/client is already a dependency):
// src/db/index.ts
import { createClient } from '@libsql/client';
import { drizzle } from 'drizzle-orm/libsql';
import * as schema from './schema';
const client = createClient({
url: process.env.DATABASE_URL!, // file:./app.db, or libsql://<db>.turso.io
authToken: process.env.DATABASE_AUTH_TOKEN,
});
export const db = drizzle(client, { schema, casing: 'snake_case' });Load the export: sqlite3 app.db < live.sql for a file, or turso db create my-app --from-dump live.sql for Turso. Then run npm run check (build and type check) and try sign-in, a page that reads data and one that writes.
- Vercel: import the repository, keep the default Next.js settings, and add the environment variables in the project's settings.
- Your own server: run it with systemd behind a reverse proxy for HTTPS — the full recipe is in Run Locally & Self-Host.
- Behind a reverse proxy, set
TOTALUM_TRUST_PROXY=1so the app reads its public address fromX-Forwarded-HostandX-Forwarded-Proto, which sign-in needs. - Login rate limits count attempts per visitor IP, read from the
CF-Connecting-IPheader that Cloudflare sends. On another host, have your proxy send it (Caddy:header_up CF-Connecting-IP {remote_host}), or changeipAddressHeadersinsrc/lib/auth.tsto your proxy's header. - Security headers (Content-Security-Policy, HSTS,
frame-ancestors) are added byworker.ts, which runs only on Workers. Elsewhere, add them in your proxy or innext.config.ts.
#Database changes after the move
New migrations still come from npm run db:generate (files in drizzle/). Apply them to your own database with Drizzle's migrator and the same _migrations table, so the ones already applied are skipped:
// scripts/migrate-own.ts — run: npx tsx --env-file=.env.prod scripts/migrate-own.ts
import { migrate } from 'drizzle-orm/libsql/migrator';
import { db } from '../src/db';
await migrate(db, { migrationsFolder: 'drizzle', migrationsTable: '_migrations' });On your own Cloudflare D1, apply each new file with npx wrangler d1 execute my-app-db --remote --file=drizzle/<file>.sql.
#Built-in features
Every totalum.* call (and totalum-sdk/* import) is a built-in feature. Keep it with the project key in the environment — it is billed to the project in credits and keeps its limits — or replace it with a provider of your choice:
| Feature | Replace with, for example |
|---|---|
totalum.email (also sends the password-reset and sign-in code e-mails) | Any e-mail API or SMTP |
The AI gateway (TOTALUM_AI_BASE_URL: OpenRouter's API for text, images, video, speech, transcription, embeddings, rerank) and older totalum.ai calls | OpenRouter with your own key: TOTALUM_AI_BASE_URL=https://openrouter.ai/api/v1 and your key in place of the project key (src/agents/model.ts), and a model id where the code names a Totalum preset (fast, tts, video, …) |
totalum.files | S3, R2 or any object storage |
totalum.payments | Your own Stripe integration |
totalum.connections (your users' own Google, Microsoft, Slack… accounts) | Your own OAuth app with each provider, or a connected-accounts service |
totalum.cron | Your host's scheduler (cron, Cloudflare Cron Triggers, Vercel Cron) calling the same /api/cron/... routes |
totalum.realtime, totalum.vectors, totalum.web, totalum.browser, totalum.pdf, totalum.scan, older totalum.speech calls | The equivalent service of your choice |
Search the code for totalum to find every call. Sign-in itself (better-auth) does not depend on Totalum.
#5. Environment variables
Download them as shown in step 1 and set them on the new host (never commit them):
| Variable | Needed for |
|---|---|
BETTER_AUTH_SECRET | Sign-in. Keep the same value, or every session ends |
TOTALUM_PROJECT_KEY | Built-in features, and the database while it stays on Totalum. Drop it once you replaced them |
TOTALUM_ENV | live (the published database) or dev |
TOTALUM_PROJECT_ID | Optional: the sign-in cookie's name. Keep it, or everyone signs in again once |
TOTALUM_SDK_API_URL | Optional: defaults to https://sdk-api.totalum.app |
TOTALUM_PUBLIC_HOSTS | Your domain (app.example.com): canonical links, sitemap and the sign-in origins |
TOTALUM_TRUST_PROXY | 1 behind a reverse proxy |
TOTALUM_OWNER_EMAIL | The address that is the app's admin once verified |
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET | "Continue with Google", when used. Add your new domain's callback (https://<domain>/api/auth/callback/google) in Google Cloud |
| Your own secrets | Whatever your app reads (Stripe keys, third-party APIs) |
DATABASE_URL, DATABASE_AUTH_TOKEN | Only with your own libSQL / Turso database, as above |
#6. Keep developing
npm ci
npm run dev # http://localhost:3100npm run dev applies pending migrations, then serves the app. With TOTALUM_ENV=dev and the project key in .env it uses the project's development database on Totalum; without them, a local database in .wrangler/. After changing src/db/schema.ts, run npm run db:generate; the next npm run dev applies the migration. npm run check runs the same build and checks Totalum runs before deploying.
The project is plain TypeScript, so any editor and any AI coding agent (Claude Code, Cursor, Codex…) can work on it. Its AGENTS.md and _system-docs/ describe the project's conventions and the built-in features, and agents read them by themselves.
Prefer to keep building with Totalum's agent too? Leave the project connected to GitHub: your commits on develop come back into Totalum before the next prompt.
