Backoffice gives your AI assistant β Claude.ai, ChatGPT, or any other β its own Linux machine that keeps working even when you're not in a conversation.
- Use any CLI β no MCPs needed.
- Run cron jobs β schedule tasks between conversations.
- Persist data β files, memory, and state survive across sessions.
- Store credentials securely β API keys live on the machine, not in the chat.
AI assistants like Claude and ChatGPT can use MCPs to access external services, but the library of available MCPs is limited. If a service doesn't have an MCP, you're stuck searching for third-party providers.
This is what Backoffice aims to solve. It gives Claude, ChatGPT, or any other AI assistant app, a remote Linux machine so that it has a command line it can use with minimal restrictions.
A simple example of something Backoffice has been useful for is Strava access. Strava has no official MCP but I have a CLI for it that I use all the time. Instead of going to a 3rd party MCP provider I just use Backoffice within Claude and tell it to install and use the Strava CLI within Backoffice.
First:
Then:
- Add
https://your-app.up.railway.app/mcpas an MCP at your favorite AI assistant. - Your assistant will prompt you for a password, this in a random string that can be found in the Railway service logs.
- Start a new conversation with your assistant. It will now have access to the remote machine through the Backoffice MCP.
The one-click install sets up this repo as a Railway app, will mount a volume on /data to persist data (see "Persisting Data" below), and sets up a health check for GET /version so that Railway monitors the health of the service using that endpoint.
claude "Read this: https://kvendrik.com/backoffice/AGENT.md"git clone git@github.com:kvendrik/backoffice.git2. Deploy to Railway
Or any other remote-machines service like Fly.io. On Railway however this works out of the box β the server reads
RAILWAY_PUBLIC_DOMAINautomatically. For other hosts, setPUBLIC_BASE_URLto your public origin.
brew install railway
railway login
railway up- Add
https://your-app.up.railway.app/mcpas an MCP. - You'll be prompted for a passphrase which you can find in the startup logs. Backoffice logs it on startup.
Start a new conversation with your assistant. It will now have access to the remote machine through the Backoffice MCP.
Backoffice comes with full OAuth. Apps like Claude.ai handle the entire flow automatically β no client ID or secret needs to be configured manually.
The OAuth consent screen requires a passphrase before issuing tokens. A passphrase is auto-generated on startup and printed to stdout. Set AUTH_PASSPHRASE to use your own.
- Persistent state (default). Tokens are saved to
/data/oauth-state.jsonand survive restarts and redeploys. Requires a/dataVolume β see "Persisting Data" below. SetOAUTH_RESET_ON_RESTART=1to disable. - In-memory state. Set
OAUTH_RESET_ON_RESTART=1to use in-memory state instead. Tokens are lost on restart and apps like Claude.ai re-authenticate automatically. - Short-lived tokens. Access tokens expire every hour. Apps like Claude.ai refresh them automatically.
| Variable | Default | Description |
|---|---|---|
AUTH_PASSPHRASE |
(random, logged on startup) | Passphrase required on the OAuth consent screen. Set this to a strong secret so it never appears in logs. |
ALLOWED_REDIRECT_URI_DOMAINS |
claude.ai |
Comma-separated list of domains that OAuth clients are allowed to register redirect URIs for. Registrations with a redirect_uri on a domain not in this list are rejected. Set to claude.ai,localhost to also allow local clients. |
OAUTH_RESET_ON_RESTART |
false |
Set to 1 to disable OAuth state persistence. Existing tokens are lost on restart and clients re-authenticate automatically. |
USE_MCP_TOKEN_AUTH |
false |
Set to 1 to replace OAuth with a single static bearer token. Simpler, but no per-client visibility in logs. The token is read from MCP_TOKEN or auto-generated and written to .mcp-token. |
MCP_TOKEN |
(auto-generated) | Static bearer token. Only used when USE_MCP_TOKEN_AUTH=1. |
PUBLIC_BASE_URL |
(derived from RAILWAY_PUBLIC_DOMAIN) |
Public origin of the server (e.g. https://your-app.up.railway.app). Required on non-Railway hosts. |
PORT |
3000 |
Port the server listens on. |
| Tool | Purpose |
|---|---|
shell |
Run any bash command on the machine. Working directory and environment persist across calls. Output is capped at 1 MB per stream by default (configurable via max_output_bytes). Credentials set via env_set are automatically injected. |
patch_file |
Apply a structured line-based patch to a file. Useful for targeted edits to specific lines in large files without rewriting the whole thing. |
env_set |
Persist an environment variable. Stored on disk and automatically injected into every shell call. Use for credentials and API keys β values are not returned to the conversation. |
env_delete |
Remove a persisted environment variable. |
memory_read |
Read the persistent memory file (/data/MEMORY.md). Called at the start of every conversation to recall context from previous sessions. |
memory_write |
Write to the persistent memory file. The AI proactively saves anything useful across conversations: installed CLIs, useful paths, environment quirks, user preferences, and how to use specific tools/APIs/services. |
memory_append |
Append content to the memory file. The simplest way to add new information β no format overhead, no context-mismatch risk. |
memory_patch |
Apply a targeted patch to the memory file using the same *** Begin Patch format as patch_file. Use for surgical replacements of known stale content. |
get_instructions |
Return the full system instructions for the MCP server. The AI can call this if it needs guidance on conventions or tool usage. |
The server runs as a non-root user (appuser). This means the OS itself enforces what the process can and can't touch.
What's protected:
- System directories (
/usr,/bin,/etc, etc.) β root-owned, unwritable - App source (
/app) β root-owned, unwritable
What's writable:
/dataβ persistent volume, owned byappuser/tmpβ ephemeral scratch space
By default Railway spins up a fresh container on every deploy. To persist data add a Volume in your Railway service settings and mount it at /data. The AI is instructed to use /data for memory and credentials (via env_set), so this path matters. See Railway's Volumes docs for details.
Packages installed via bun install -g go to /data/bun and packages installed via brew install go to /data/homebrew β both paths are on the persistent volume, so installed tools survive redeploys automatically.
Backoffice keeps logs of all tool calls (includes caller oauth details) and results in /data/log.jsonl. Analyzing this file can help you figure out how to improve your setup:
claude "Here are the logs from the Backoffice railway server and show how the AI assistant has been using Backoffice. Tell me what you notice.\n\n---\n\n$(railway ssh -- cat /data/log.jsonl)"