Multi-Tenant App
This example shows how to build a multi-tenant application where each user gets their own githosted repo. Repo IDs are stored in your database so agents can pick up work without knowing slugs.
Full example
Section titled “Full example”import { Client, type RepoRef } from "@githosted/sdk";
// -- Server side: repo provisioning --
const client = new Client({ token: process.env.GITHOSTED_TOKEN,});
/** * Called when a new user signs up. * Creates a dedicated repo and returns the stable ID for storage. */async function provisionUserRepo(userId: string): Promise<string> { const repo = await client.createRepo(`user-${userId}`, { slug: `users/${userId}`, });
const repoId = repo.id!; // e.g. "rp_a1b2c3d4"
// Write initial scaffolding await repo.transaction("Initialize user workspace", async (tx) => { await tx.write( "config.json", JSON.stringify({ userId, createdAt: new Date().toISOString(), model: "claude-3", maxTokens: 4096, }, null, 2), ); await tx.write("conversations/.gitkeep", ""); await tx.write("artifacts/.gitkeep", ""); });
// Store the stable repo ID in your database await db.users.update(userId, { repoId });
return repoId;}
/** * Look up a user's repo by the ID stored in your database. * Using { id } is more stable than slugs, which users might rename. */function getUserRepo(repoId: string) { return client.repo({ id: repoId });}Agent picking up work by repo ID
Section titled “Agent picking up work by repo ID”Agents receive a repo ID from a job queue and operate on it directly:
import { Client, isStaleHeadError } from "@githosted/sdk";
interface AgentJob { repoId: string; // e.g. "rp_a1b2c3d4" conversationId: string; userMessage: string;}
async function processJob(job: AgentJob) { const client = new Client({ token: process.env.GITHOSTED_TOKEN });
// Reference the repo by stable ID -- no slug lookup needed const repo = client.repo({ id: job.repoId });
// Read the user's config const configFile = await repo.read("config.json"); const config = JSON.parse(configFile.content);
// Read conversation history (if it exists) let history: Array<{ role: string; content: string }> = []; try { const historyFile = await repo.read( `conversations/${job.conversationId}.json`, ); history = JSON.parse(historyFile.content); } catch { // First message in this conversation }
// Run your AI model const response = await callModel({ model: config.model, maxTokens: config.maxTokens, messages: [...history, { role: "user", content: job.userMessage }], });
// Save the updated conversation and any artifacts atomically await repo.transaction( `Agent response for ${job.conversationId}`, async (tx) => { const updatedHistory = [ ...history, { role: "user", content: job.userMessage }, { role: "assistant", content: response.text }, ]; await tx.write( `conversations/${job.conversationId}.json`, JSON.stringify(updatedHistory, null, 2), );
// If the agent produced artifacts, save them too for (const artifact of response.artifacts ?? []) { await tx.write( `artifacts/${artifact.filename}`, artifact.content, ); } }, );}Listing a user’s files from an API endpoint
Section titled “Listing a user’s files from an API endpoint”import { Client } from "@githosted/sdk";
// Express / Next.js API route handlerasync function handleListFiles(req, res) { const userId = req.auth.userId;
// Look up the repo ID from your database const user = await db.users.findById(userId); if (!user?.repoId) { return res.status(404).json({ error: "No workspace found" }); }
const client = new Client({ token: process.env.GITHOSTED_TOKEN }); const repo = client.repo({ id: user.repoId });
const files = await repo.ls(req.query.path ?? ""); res.json({ files });}Viewing a user’s commit history
Section titled “Viewing a user’s commit history”async function handleGetHistory(req, res) { const user = await db.users.findById(req.auth.userId); const repo = client.repo({ id: user.repoId });
// Get recent commits, optionally filtered to a path const commits = await repo.log({ path: req.query.path, limit: 20, });
res.json({ commits: commits.map((c) => ({ hash: c.hash, subject: c.subject, author: c.authorName, date: c.committedAt.toISOString(), })), });}Key points
Section titled “Key points”- Use
{ id: "rp_xxx" }for stable references. Slugs can change if the workspace or repo is renamed. Store therepo.idin your database aftercreateRepoand use it for all subsequent access. - One repo per tenant gives you natural isolation, per-user commit history, and the ability to diff any user’s state over time.
- Agents are stateless. They receive a repo ID from your job queue, do their work, commit results, and exit. The repo is the durable state.
- Transactions keep multi-file writes atomic — a conversation update and its artifacts land in a single commit, so readers never see a partial state.