Docs
BlogHomeStart building

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

PartWhere it livesWhat you do
CodeThe project's repositoryDownload it or sync it to GitHub
Database (tables, rows, users, sessions)A SQLite database per environmentExport it as SQL, import it where you run the app
Uploaded filesTotalum's file storageKeep them there, or copy them to your own storage
Sign-inbetter-auth, in your code and your databaseMoves with them. Keep the same BETTER_AUTH_SECRET so everyone stays signed in
Built-in features (totalum.* calls)Totalum, through the project keyKeep 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.
Keep the Totalum project until you have moved

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; develop holds every version, main the published one.
  • API: Download Source Code, and the environment files with Get Project Env.
bash
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 it complete, then Download Backup.
bash
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.sql

A 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:

ts
// 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:

bash
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.sql

In 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:

bash
npx wrangler secret put BETTER_AUTH_SECRET
npx opennextjs-cloudflare build
npx wrangler deploy

Attach 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):

ts
// 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=1 so the app reads its public address from X-Forwarded-Host and X-Forwarded-Proto, which sign-in needs.
  • Login rate limits count attempts per visitor IP, read from the CF-Connecting-IP header that Cloudflare sends. On another host, have your proxy send it (Caddy: header_up CF-Connecting-IP {remote_host}), or change ipAddressHeaders in src/lib/auth.ts to your proxy's header.
  • Security headers (Content-Security-Policy, HSTS, frame-ancestors) are added by worker.ts, which runs only on Workers. Elsewhere, add them in your proxy or in next.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:

ts
// 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:

FeatureReplace 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 callsOpenRouter 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.filesS3, R2 or any object storage
totalum.paymentsYour 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.cronYour 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 callsThe 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):

VariableNeeded for
BETTER_AUTH_SECRETSign-in. Keep the same value, or every session ends
TOTALUM_PROJECT_KEYBuilt-in features, and the database while it stays on Totalum. Drop it once you replaced them
TOTALUM_ENVlive (the published database) or dev
TOTALUM_PROJECT_IDOptional: the sign-in cookie's name. Keep it, or everyone signs in again once
TOTALUM_SDK_API_URLOptional: defaults to https://sdk-api.totalum.app
TOTALUM_PUBLIC_HOSTSYour domain (app.example.com): canonical links, sitemap and the sign-in origins
TOTALUM_TRUST_PROXY1 behind a reverse proxy
TOTALUM_OWNER_EMAILThe 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 secretsWhatever your app reads (Stripe keys, third-party APIs)
DATABASE_URL, DATABASE_AUTH_TOKENOnly with your own libSQL / Turso database, as above

#6. Keep developing

bash
npm ci
npm run dev          # http://localhost:3100

npm 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.