Created
August 14, 2025 12:46
-
-
Save spookyuser/5699ddd5a6e2c62e316e7d4b6d5b456f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Backup InstantDB data to a single gzip-compressed JSON file. | |
Usage: | |
pnpm backup:instantdb | |
Output: | |
backups/instantdb/instantdb-backup-<ISO_TIMESTAMP>.json.gz | |
Requirements: | |
- Environment vars: INSTANT_APP_ID, INSTANT_ADMIN_TOKEN | |
- Node 18+ (global fetch) | |
*/ | |
import { mkdir, writeFile } from "node:fs/promises" | |
import path from "node:path" | |
import { promisify } from "node:util" | |
import { gzip as gzipCallback } from "node:zlib" | |
import schema from "../packages/db/instant.schema" | |
type SchemaJson = { | |
entities: Record<string, unknown> | |
links?: Record<string, unknown> | |
rooms?: Record<string, unknown> | |
} | |
type InstantQueryResponse = any | |
async function getSchemaJson(): Promise<SchemaJson> { | |
return { | |
entities: (schema as any).entities, | |
links: (schema as any).links, | |
rooms: (schema as any).rooms, | |
} | |
} | |
function getHeaders(appId: string, adminToken: string): Record<string, string> { | |
return { | |
"content-type": "application/json", | |
Authorization: `Bearer ${adminToken}`, | |
"App-Id": appId, | |
// Version headers are optional for the Admin API, omit to reduce coupling | |
} | |
} | |
async function queryCollection( | |
baseUrl: string, | |
headers: Record<string, string>, | |
collection: string, | |
): Promise<InstantQueryResponse> { | |
const body = { query: { [collection]: {} } } | |
const res = await fetch(`${baseUrl}/admin/query`, { | |
method: "POST", | |
headers, | |
body: JSON.stringify(body), | |
}) | |
if (!res.ok) { | |
const text = await res.text() | |
throw new Error(`Failed to query ${collection}: ${res.status} ${text}`) | |
} | |
return res.json() | |
} | |
async function listStorageFiles( | |
baseUrl: string, | |
headers: Record<string, string>, | |
): Promise<any> { | |
const res = await fetch(`${baseUrl}/admin/storage/files`, { | |
method: "GET", | |
headers, | |
}) | |
if (!res.ok) { | |
const text = await res.text() | |
throw new Error(`Failed to list storage files: ${res.status} ${text}`) | |
} | |
return res.json() | |
} | |
function nowStamp(): string { | |
return new Date().toISOString().replace(/[:.]/g, "-") | |
} | |
async function main() { | |
const repoRoot = path.resolve(__dirname, "..") | |
const appId = process.env.INSTANT_APP_ID | |
const adminToken = process.env.INSTANT_ADMIN_TOKEN | |
if (!appId || !adminToken) { | |
throw new Error( | |
"Missing INSTANT_APP_ID or INSTANT_ADMIN_TOKEN in environment. Use dotenvx or export them before running.", | |
) | |
} | |
const baseUrl = process.env.INSTANT_BASE_URL || "https://api.instantdb.com" | |
const headers = getHeaders(appId, adminToken) | |
const schemaJson = await getSchemaJson() | |
const timestamp = nowStamp() | |
const backupDir = path.join(repoRoot, "backups", "instantdb") | |
await mkdir(backupDir, { recursive: true }) | |
// Dump entities sequentially for simplicity and stability | |
const entityNames = Object.keys(schemaJson.entities || {}) | |
const perEntityResults: Record<string, InstantQueryResponse> = {} | |
for (const name of entityNames) { | |
const data = await queryCollection(baseUrl, headers, name) | |
perEntityResults[name] = data | |
} | |
// Dump storage file metadata | |
let storageFiles: any | null = null | |
try { | |
storageFiles = await listStorageFiles(baseUrl, headers) | |
} catch (e) { | |
// Not fatal if storage API is unavailable | |
storageFiles = { error: String(e) } | |
} | |
// Build one combined payload (minified for smaller gzip and memory usage) | |
const combined = { | |
metadata: { | |
createdAt: new Date().toISOString(), | |
appId, | |
baseUrl, | |
entityCount: entityNames.length, | |
entities: entityNames, | |
}, | |
schema: schemaJson, | |
entities: perEntityResults, | |
storageFiles, | |
} | |
const combinedJson = JSON.stringify(combined) | |
const gzip = promisify(gzipCallback) | |
const gz = await gzip(Buffer.from(combinedJson), { level: 9 } as any) | |
const outPath = path.join(backupDir, `instantdb-backup-${timestamp}.json.gz`) | |
await writeFile(outPath, gz) | |
// eslint-disable-next-line no-console | |
console.log(`Backup written to ${outPath}`) | |
} | |
main().catch((err) => { | |
// eslint-disable-next-line no-console | |
console.error(err) | |
process.exit(1) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment