Skip to content

Instantly share code, notes, and snippets.

@jeapostrophe
Last active March 20, 2026 03:33
Show Gist options
  • Select an option

  • Save jeapostrophe/b14377a18ba33ae06c5604b4d5cd1089 to your computer and use it in GitHub Desktop.

Select an option

Save jeapostrophe/b14377a18ba33ae06c5604b4d5cd1089 to your computer and use it in GitHub Desktop.
Chiron storage server — self-hosted JSON read/write server for the Chiron iOS app
// chiron-storage.ts — Self-hosted Chiron storage server
//
// A single-file Deno server that reads/writes JSON files from a data/ directory.
// No logic, no SPA serving, no dependencies beyond Deno std.
//
// Usage:
// deno run --allow-net --allow-read=./data --allow-write=./data chiron-storage.ts
//
// Environment variables:
// CHIRON_DATA — path to the data directory (default: ./data)
// PORT — port to listen on (default: 4114)
//
// Endpoints:
// GET /data — list all stored keys (for sync)
// GET /data/:key — read a single JSON file (e.g. /data/profile)
// PUT /data/:key — write a single JSON file (body = JSON)
// GET /data/:category/:ym — read a monthly JSON file (e.g. /data/daily/2026-03)
// PUT /data/:category/:ym — write a monthly JSON file (body = JSON array)
//
// Categories: daily, workouts, weekly
//
// Pair with the Chiron app: Settings -> Use Remote Server -> enter this server's URL
import { ensureDir } from "jsr:@std/fs";
import { join } from "jsr:@std/path";
const DATA_DIR = Deno.env.get("CHIRON_DATA") ?? "./data";
const PORT = Number(Deno.env.get("PORT") ?? 4114);
await ensureDir(DATA_DIR);
for (const sub of ["daily", "workouts", "weekly"]) {
await ensureDir(join(DATA_DIR, sub));
}
const CORS: Record<string, string> = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, PUT, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
};
function json(data: unknown, status = 200): Response {
return new Response(JSON.stringify(data), {
status,
headers: { "Content-Type": "application/json", ...CORS },
});
}
async function readJSON(path: string): Promise<unknown> {
try {
return JSON.parse(await Deno.readTextFile(path));
} catch (e) {
if (e instanceof Deno.errors.NotFound) return null;
throw e;
}
}
const MONTHLY_CATEGORIES = ["daily", "workouts", "weekly"];
/** List all stored keys in the data directory. */
async function listKeys(): Promise<string[]> {
const keys: string[] = [];
// Single files in data/
for await (const entry of Deno.readDir(DATA_DIR)) {
if (entry.isFile && entry.name.endsWith(".json")) {
keys.push(entry.name.replace(/\.json$/, ""));
}
}
// Monthly files in data/{category}/
for (const cat of MONTHLY_CATEGORIES) {
const dir = join(DATA_DIR, cat);
try {
for await (const entry of Deno.readDir(dir)) {
if (entry.isFile && entry.name.endsWith(".json")) {
keys.push(`${cat}/${entry.name.replace(/\.json$/, "")}`);
}
}
} catch (e) {
if (!(e instanceof Deno.errors.NotFound)) throw e;
}
}
return keys.sort();
}
Deno.serve({ port: PORT }, async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: CORS });
const url = new URL(req.url);
const path = url.pathname.replace(/\/+$/, "");
// List all keys: GET /data
if (path === "/data" && req.method === "GET") {
return json({ keys: await listKeys() });
}
const parts = path.replace(/^\/data\/?/, "").split("/").filter(Boolean);
if (parts.length === 0) return json({ error: "Not found" }, 404);
// Single file: GET/PUT /data/:key
if (parts.length === 1) {
const file = join(DATA_DIR, `${parts[0]}.json`);
if (req.method === "GET") return json(await readJSON(file));
if (req.method === "PUT") {
const body = await req.text();
JSON.parse(body); // validate
await Deno.writeTextFile(file, body);
return json(JSON.parse(body));
}
}
// Month file: GET/PUT /data/:category/:yearMonth
if (parts.length === 2) {
const [category, ym] = parts;
if (!MONTHLY_CATEGORIES.includes(category)) {
return json({ error: "Invalid category" }, 400);
}
const file = join(DATA_DIR, category, `${ym}.json`);
if (req.method === "GET") return json((await readJSON(file)) ?? []);
if (req.method === "PUT") {
const body = await req.text();
const arr = JSON.parse(body);
if (!Array.isArray(arr)) return json({ error: "Expected array" }, 400);
await Deno.writeTextFile(file, JSON.stringify(arr, null, 2));
return json(arr);
}
}
return json({ error: "Not found" }, 404);
});
console.log(`Chiron storage server listening on :${PORT}`);
console.log(`Data directory: ${DATA_DIR}`);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment