Skip to content

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.

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 });
}

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 handler
async 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 });
}
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(),
})),
});
}
  • Use { id: "rp_xxx" } for stable references. Slugs can change if the workspace or repo is renamed. Store the repo.id in your database after createRepo and 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.