| name | wa-persistence |
| description | Stage 6 (optional). Choose where conversation history and OAuth tokens live. Default is SQLite on Render's ephemeral disk; production needs Render Disk, Supabase, or Render Postgres. |
wa-persistence โ Where the bot's memory lives
Goal: pick the right storage backend for the user's situation. Default is fine for most personal bots; production / shared bots need an upgrade.
Speak Hebrew. Explain trade-offs in plain language. Don't push them to the most complex option.
The four options
1. SQLite ephemeral (default โ what wa-build ships)
- File at
./bot.db on Render's filesystem.
- โ
Zero setup, zero cost.
- โ Wiped on every redeploy on Render free tier (free tier has no persistent disk).
- โ Wiped on container restart.
- ๐ก Works for testing, demos, "I'll redo this later."
2. SQLite on Render Starter + Persistent Disk (~$7.25/mo total)
- Same SQLite file, but mounted on a 1GB persistent disk.
- โ
Survives redeploys and restarts.
- โ
Same simple code as default โ no migration.
- โ
Bonus: always-on โ Starter plan removes the 15-min idle sleep, so first reply after a quiet period is instant.
- โ Disk only attaches to a single instance. If Render scales horizontally (Standard+ plans), only one container can write.
- ๐ก Right answer for personal bots that store anything sensitive (OAuth tokens, conversation history) โ and the right answer the moment the user wires Calendar/Gmail.
โ ๏ธ Pricing reality: Persistent disks require Starter plan ($7/mo) as a prerequisite โ they're not supported on Free tier. The disk addon itself is ~$0.25/mo for 1GB. Total: ~$7.25/mo. (Earlier versions of this skill said "$1/mo" โ that was wrong; Render won't even let you attach a disk to a free-tier service. The API returns disk not supported for free tier service.)
โ ๏ธ Plan upgrades require the dashboard, not the API. PATCH /v1/services with {serviceDetails: {plan: "starter"}} returns HTTP 500. Walk the user through clicking Settings โ Instance Type โ Starter โ Save in the Render dashboard. Confirm the change took effect by polling GET /v1/services/{id} and checking serviceDetails.plan == "starter" before attempting to attach the disk.
3. Supabase Postgres (free tier, then $25/mo)
- Managed Postgres, accessed via
DATABASE_URL.
- โ
Free tier (500 MB, paused after 7 days idle โ wakes on demand).
- โ
Survives everything. Visible in Supabase UI for debugging.
- โ Requires external account, slight extra latency (~30ms).
- ๐ก Right answer if user already uses Supabase, or wants to share data with another app.
4. Render Postgres ($7/mo)
- Render-native Postgres, internal network (low latency).
- โ
Same region as the bot, fast.
- โ
Daily backups.
- โ Costs from day one.
- ๐ก Right answer for a paid bot that needs reliability.
The conversation
ืจืืฆื ืืืืืื ืืืคื ืืืื ืฉืืืจ ืืช ืืืืกืืืจืื ืฉื ืืฉืืืืช (ืืืืจืฉืืืช ืืืืื ืืืืฆืื ืืื)?
ืืจืืข ืืื ืฉืืืจ ืืงืืืฅ ืืื ื ืฉื ืืืง ืืฉืืืื ืืชืขืืื. ืืฉืืืื ืื ืืืื ืืืืืช ืืกืืจ, ืื ืื โ ืชืืื ืืื ืฉืืชื ืขืืฉื ืืืชื.
4 ืืคืฉืจืืืืช:
ื. ืืืื ื ืฉืืฉ ืืจืืข โ ืืื ื, ื ืืืง ืืื ืขืืืื. ืืื ืืืืืงืืช.
ื. Render Starter + ืืืกืง ืงืืืข โ ืืขืจื $7.25 ืืืืืฉ. ื ืฉืืจ ืชืืื ืืืืื ืื ื ืจืื. ืืื ืงื ืืจืืข ืฉืืืืจืื ืืืื/ืืืื.
ื. Supabase โ ืืื ื ืืืชืืื, $25 ืืฉืืื. ืฉืืจืืช ืืืฆืื ื ืขื ืืืฉืง ืืคื.
ื. Postgres ื-Render โ $7 ืืืืืฉ. ืืื ืืืืจ ืืืฆืื, ืขื ืืืืืืื.
ืื ืืชืืื ืื?
Default recommendation per archetype (offer it but let user override):
personal_assistant โ ื (Render Disk)
customer_support โ ื (Render Postgres)
knowledge_bot โ ื (Render Disk)
scheduler โ ื (Render Postgres) if multi-tenant, else ื
Migration steps
Option ื โ Render Starter + Persistent Disk
Order matters. Do these in sequence โ skipping or reordering will fail with the errors noted.
-
Verify (or trigger) the plan upgrade to Starter โ disk-attach will fail on Free with disk not supported for free tier service (HTTP 400):
curl -s "https://api.render.com/v1/services/$RENDER_SERVICE_ID" \
-H "Authorization: Bearer $RENDER_API_KEY" \
| python -c "import sys,json; print(json.load(sys.stdin)['serviceDetails']['plan'])"
If output is free, hand off to the user with this exact instruction:
ืชืคืชื ืืช https://dashboard.render.com/web/$RENDER_SERVICE_ID/settings, ืืืื ื-Instance Type, ืชืืืฅ Change, ืชืืืจ Starter ($7/ืืืืฉ), ืชืฉืืืจ. ืชืืื ืื "ืฉืืืจื" ืืฉืืืืื' ืืืขืื ืืืืจ Starter.
Wait for confirmation, then re-poll serviceDetails.plan until it's starter.
-
Attach the disk โ endpoint is top-level /v1/disks, NOT /v1/services/{id}/disks or /v1/services/{id}/disk (both return 404):
curl -sX POST "https://api.render.com/v1/disks" \
-H "Authorization: Bearer $RENDER_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"serviceId\": \"$RENDER_SERVICE_ID\", \"name\": \"bot-data\", \"mountPath\": \"/var/data\", \"sizeGB\": 1}"
Successful response is HTTP 201 with the disk ID.
-
Update DB_PATH env var to the new mount path:
curl -sX PUT "https://api.render.com/v1/services/$RENDER_SERVICE_ID/env-vars/DB_PATH" \
-H "Authorization: Bearer $RENDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"value": "/var/data/bot.db"}'
-
Trigger a redeploy โ the env var change alone doesn't always retrigger:
curl -sX POST "https://api.render.com/v1/services/$RENDER_SERVICE_ID/deploys" \
-H "Authorization: Bearer $RENDER_API_KEY" \
-H "Content-Type: application/json" -d '{}'
-
Wait for live, then verify curl $RENDER_URL/healthz returns 200. Allow ~30s for cold-start after the deploy reports live.
โ ๏ธ Critical: do NOT update DB_PATH before the disk is attached. If you do, the bot will crash-loop on startup with sqlite3.OperationalError: unable to open database file because /var/data/ doesn't exist as a directory yet. Fix order: disk first, then env var.
The history is empty after the first migrated deploy (the old ephemeral disk is gone). Tell the user:
ืืืืกืืืจืื ืืงืืืืช ืชืืขืื ืืขืืืื ืืื (ืื ืืื ืืืคื ืืฉืืืจ ืืืชื). ืืืืื ืืืืื โ ื ืฉืืจ.
Option ื โ Supabase
-
Walk user through:
- https://supabase.com/dashboard โ New Project (Free tier)
- Wait ~2min for provision
- Settings โ Database โ Connection string โ URI (with password)
-
Save DATABASE_URL as Render env var.
-
Update bot/database.py: replace sqlite3 with psycopg[binary]. Provide drop-in module:
import os, time
import psycopg
from psycopg.rows import dict_row
_dsn = os.environ["DATABASE_URL"]
def init_db():
with psycopg.connect(_dsn) as c, c.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS conversations (
id BIGSERIAL PRIMARY KEY,
chat_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL,
created_at BIGINT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_conv_chat ON conversations(chat_id, id);
CREATE TABLE IF NOT EXISTS seen_messages (
msg_id TEXT PRIMARY KEY,
created_at BIGINT NOT NULL
);
""")
def seen_message(msg_id: str) -> bool:
with psycopg.connect(_dsn) as c, c.cursor() as cur:
try:
cur.execute("INSERT INTO seen_messages (msg_id, created_at) VALUES (%s, %s)",
(msg_id, int(time.time())))
return False
except psycopg.errors.UniqueViolation:
return True
def append(chat_id: str, role: str, content: str):
with psycopg.connect(_dsn) as c, c.cursor() as cur:
cur.execute("INSERT INTO conversations (chat_id, role, content, created_at) "
"VALUES (%s, %s, %s, %s)", (chat_id, role, content, int(time.time())))
def tail(chat_id: str, n: int = 20) -> list[dict]:
with psycopg.connect(_dsn) as c, c.cursor(row_factory=dict_row) as cur:
cur.execute("SELECT role, content FROM conversations WHERE chat_id=%s "
"ORDER BY id DESC LIMIT %s", (chat_id, n))
rows = cur.fetchall()
return list(reversed(rows))
- Add
psycopg[binary]==3.2.* to requirements.txt. Remove sqlite-only code paths.
- Push, wait for redeploy, verify.
Option ื โ Render Postgres
- Create the Postgres instance via Render API:
curl -sX POST "https://api.render.com/v1/postgres" \
-H "Authorization: Bearer $RENDER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "wa-bot-db", "ownerId": "'$RENDER_OWNER_ID'", "plan": "starter", "region": "frankfurt", "version": "16"}'
-
Wait for status: available, fetch the internal connection string from GET /v1/postgres/{id}/connection-info.
-
Set DATABASE_URL env on the bot service. Same database.py swap as Supabase. Push, redeploy, verify.
After migration
Update .wa-state.json:
{ "persistence": "render_disk" | "supabase" | "render_postgres" | "ephemeral" }
Tell the user:
ืืืืกืืืจืื ืืขืช ื ืฉืืจืช ื-{choice}. ืืืื ืืืืืจ ืฉืืืืช ืื ืืืจื ืขืืืื ืื.
Edge cases
- User picked option ื after already running for weeks. Old SQLite data lives on the ephemeral disk only until next redeploy. If they want to preserve it: scp the file out before adding the disk, then put it back at
/var/data/bot.db on first boot. Walk them through, but for non-technical users it's usually fine to start fresh.
- Supabase paused. Free Supabase pauses after 7 days idle. First message after pause takes ~30s to wake. Document this.
- Migration to a different option later. Always possible. Conversation table schema is identical across all options. OAuth tokens table will need migrating too โ write a one-time script if user switches.