Stateful Apps on Tako: SQLite, Uploads, and the TAKO_DATA_DIR Contract
The first thing most side projects outgrow isn’t their server — it’s the assumption that they don’t need persistent storage.
You start with a stateless API. Static responses, external auth, everything in memory. Then someone asks for user preferences, or you want to store uploaded avatars, or you need to track some simple counts. And suddenly you’re pricing out managed PostgreSQL.
On a $5 VPS, that’s your entire hosting budget.
Tako ships TAKO_DATA_DIR: a persistent, per-app directory that outlives deploys and rolling restarts. SQLite, file uploads, queue data — anything that lives in a file works here, without an external service.
The contract
TAKO_DATA_DIR is an environment variable pointing to a directory Tako owns and preserves. It’s set automatically in both dev and production:
| Environment | Path |
|---|---|
tako dev | .tako/data/app/ (inside your project) |
tako deploy | /opt/tako/data/apps/{app}/data/app/ (on the server) |
You don’t create this directory — Tako does. It persists across:
- Deploys — rolling restarts swap the release directory, not the data directory
- Server restarts and
tako-serverupgrades - Scale-to-zero idle cycles — the directory is on disk, not in process memory
It’s only cleaned up when you explicitly delete the app.
SQLite without a managed database
SQLite is underrated for side projects. It’s fast, reliable, needs zero infrastructure, and scales comfortably to millions of rows on any modern VPS. The only catch is that most deploy tools don’t give you a reliable place to put the file.
TAKO_DATA_DIR is that place.
import { Database } from "bun:sqlite";
import { join } from "path";
import { dataDir } from "../tako.gen";
const db = new Database(join(dataDir, "app.db"));
db.run(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
body TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT (unixepoch())
)
`);
export default async function fetch(req: Request) {
if (req.method === "POST" && new URL(req.url).pathname === "/notes") {
const { body } = await req.json();
db.run("INSERT INTO notes (body) VALUES (?)", [body]);
return new Response("ok");
}
const notes = db.query("SELECT * FROM notes ORDER BY created_at DESC").all();
return Response.json(notes);
}
The database file lives at $TAKO_DATA_DIR/app.db. Deploy a new version and the release directory swaps, but TAKO_DATA_DIR stays put. Your rows are exactly where you left them.
File uploads
The same pattern applies to any file-based storage:
import { writeFile, mkdir } from "fs/promises";
import { join } from "path";
import { dataDir } from "../tako.gen";
const uploadsDir = join(dataDir, "uploads");
await mkdir(uploadsDir, { recursive: true });
export default async function fetch(req: Request) {
if (req.method === "POST" && new URL(req.url).pathname === "/upload") {
const formData = await req.formData();
const file = formData.get("file") as File;
await writeFile(join(uploadsDir, file.name), Buffer.from(await file.arrayBuffer()));
return Response.json({ path: `/files/${file.name}` });
}
// serve files from uploadsDir...
}
Uploaded files persist across deploys. New releases start, old ones drain — the files are untouched.
Dev/prod parity
In development, tako dev sets TAKO_DATA_DIR to .tako/data/app/ inside your project directory. Same env var, same code path, different location. No mocking, no special cases.
If you want a clean local state, delete .tako/data/app/ — the same reasoning applies in production: the data persists until you intentionally clear it.
Run tako typegen and the generated tako.gen.ts exports dataDir as a typed value alongside your secrets, so your editor knows it’s always available.
Where this doesn’t replace managed infrastructure
TAKO_DATA_DIR is a single-server guarantee. If you’re running the same app across multiple servers, each server has its own independent data directory — they don’t sync. For multi-server setups you’ll want either:
- An external database (Turso, PlanetScale, Neon)
- SQLite replication (LiteFS or Litestream) pointed at
TAKO_DATA_DIR - Architecture that avoids shared mutable state
For most side projects on a single server, none of that is necessary. A SQLite file in TAKO_DATA_DIR handles the load, survives the deploys, and costs nothing extra.
Try it
TAKO_DATA_DIR is set automatically on every deploy — no configuration required.
tako deploy
# on the server, your app sees:
# TAKO_DATA_DIR=/opt/tako/data/apps/{app}/data/app
See the deployment docs for the full setup, the development guide for how data directories behave locally, and the CLI reference for app lifecycle commands including tako app delete.