# Obsidian Arc: an AI chat and API gateway that ships as one binary

> Obsidian Arc is a self-hosted, multi-user AI chat service and API gateway that runs as a single Go binary with an embedded Vue frontend on SQLite or PostgreSQL, with provider routing, usage quotas, a plugin system and an admin terminal reachable over SSH. It suits a team that wants to run its own gateway for several users, and it carries a large surface of administrative policy that you have to decide about.

**OnyxAxisOwO/ObsidianArc** — A self-hosted AI platform with a Go server, multi-provider routing, usage tracking and an admin console, with chat and agentic work modes.

- Repository: https://github.com/OnyxAxisOwO/ObsidianArc
- Website: https://obsidianarc.pages.dev
- Stars: 623 · Forks: 13
- Language: Go
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/onyxaxisowo-obsidianarc

## One binary, one database, no extra services

Obsidian Arc is a self-hosted multi-user AI chat service and API gateway. The whole application runs as a single Go binary with an embedded Vue 3 single page application frontend, and it uses SQLite or PostgreSQL for persistence, requiring no Redis, message queue or external cache.

That constraint shapes the whole design, and it is the reason the deployment story is a single container. The repository is correspondingly compact for what it does: cmd/ for the entry points, internal/ for the server, web/ for the Vue frontend, plugins/ for the optional plugin tree, sdk/ for the client SDK, scripts/, docs/ and the container files, alongside a Dockerfile and a Dockerfile.release for the two image flavours. There are also AGENTS.md, CLAUDE.md and GEMINI.md, which are the instructions files for coding agents, a detail that tells you the project expects to be worked on with an assistant in the loop.

The Go dependencies are small and purposeful. pgx/v5 is the PostgreSQL driver, modernc.org/sqlite is the pure Go SQLite implementation, golang.org/x/crypto is there for the cryptography, and tetratelabs/wazero is a WebAssembly runtime, which is the part that most deserves a look before you deploy it, because it is the sandboxing story for anything that runs plugin code.

The Go version required is 1.27.2, and the release history is short so far, with v0.10.0 published on 2026-10-10. The repository is not archived and the last push was on 2026-10-18.

## Provider routing, and three gateway dialects for the same backend

The aggregation layer is the core of the product. Obsidian Arc is compatible with the Anthropic Messages API and with interfaces that speak the OpenAI protocol, which the documentation names as OpenAI, DeepSeek, xAI, OpenRouter, Groq, Ollama and vLLM. A single provider can host several models, and there are two levels of indirection on top of that: model mapping routing, where an external model ID is routed to the underlying actual model ID, and model aliases, which use an api_name.

Streaming is handled with Server-Sent Events, carrying both the streamed text and the reasoning chain, with the reasoning effort and thinking budget settings exposed. Two behaviours matter for an interactive product. When the client cancels a request, the data being sent upstream stops immediately, so a user who navigates away does not leave a bill running. And a model can be switched in the middle of a single conversation, which is only possible because the routing layer is separate from the conversation state.

The developer gateway under /v1 is where this becomes useful to other tools. It authenticates with an API key and exposes POST /v1/chat/completions for OpenAI compatibility, POST /v1/messages and POST /v1/messages/count_tokens for Anthropic compatibility, which the documentation notes supports Claude Code, POST /v1/responses for OpenAI Responses compatibility, which supports tools such as Codex, POST /v1/images/generations for image generation, and GET /v1/models and GET /v1/models/{id} for the model list and details. Tool calling and function calling are handled by a unified protocol translation, so a client written against one dialect can reach a provider that speaks another.

## Quota accounting with database locks, and the credit economy around it

Every conversation and every API call is written to a usage ledger, and the limits are enforced over three periods: five hours, a week and a month, applied to request counts, token consumption or credits. The implementation detail that matters is that database row locks are used to avoid concurrent overspend, which is the failure mode a naive counter has when several requests arrive at once.

Around that ledger there is a whole economy of adjustments. Administrators can batch generate redemption codes, or send reset cards directly to an account so a user can restore their allowance on their own schedule. There is an extra allowance, a bonus credit line, that the user may choose to prioritise or not, and which the administrator can switch on or off, restrict to particular models, set an expiry on, and decide whether the total is public. There is also a reserve allowance that only comes into play once everything else is spent, and a daily check-in that grants a bonus credit or a reset card for consecutive or cumulative attendance.

Two related subsystems are worth noting. The Image Lab handles image generation models with aspect ratio switching, lightbox zoom and download, and the admin side can mark a model's image generation capability independently. The health monitor records uptime and health state, probes idle models on a timer, trips a circuit breaker after consecutive failures, and raises a warning below the health.warn_below threshold. For non-admin requests the upstream information is masked, so a user cannot learn which provider and endpoint is behind a model.

The same masking logic appears in the leaderboard, where a nickname, an anonymous label or a fixed identifier can be used instead of a username.

## An admin terminal you reach over SSH, scoped to what each user may do

The management workbench is a modular admin interface with grouped navigation, responsive tabs and automatic draft saving of settings under the settingsDraft key.

The unusual part is the terminal in the account menu. It is a command line interface, available to every account, and a user group can switch it off. The rule that makes it safe is stated plainly: each person can only run what they could already do from the page. A regular user manages their own profile, keys, conversations, projects, allowance, feedback and image generation records. An administrator additionally gets the back-end commands their permissions cover, with commands named user, group, model, health and usage.

It covers every administration interface and every account-side endpoint that can be set or changed, with multi-tab support, bilingual help, tab completion and JSON output. When OBSIDIAN_SSH_ADDR is set, the terminal is reachable over SSH with ssh username@domain, and what you connect to is the terminal rather than a server shell. It can also be invoked as a single command by a script.

That combination, an HTTP API for scripts and an SSH surface for humans, is a coherent answer to the usual complaint that admin consoles are either buttons or a raw shell, and neither is scriptable.

The frontend is careful in a way that is worth naming. Rendering uses Vue template interpolation and not innerHTML or v-html, upstream API keys are encrypted in the database, and the interface offers a light theme, a dark theme and a setting that follows the system.

## Running it: the binary, the environment variables and the container

The simplest start is the binary itself:

```bash
./obsidian-arc
```

It listens on port 8080 by default and stores its data in the ./data directory, creating the SQLite database automatically, and the first account to register becomes the administrator. You can set that account explicitly at startup instead:

```bash
OBSIDIAN_ADMIN_USER=admin OBSIDIAN_ADMIN_PASSWORD='YourStrongPassword' ./obsidian-arc
```

The environment variable table is where the operational decisions live. OBSIDIAN_ADDR is the listen address and port, defaulting to :8080. OBSIDIAN_DB_DRIVER selects sqlite or postgres, and OBSIDIAN_DB_DSN is the connection string, defaulting to ./data/obsidian.db and required for PostgreSQL. OBSIDIAN_SECRET_KEY is the key that encrypts the upstream API keys, generated automatically inside ./data, and it must stay the same across instances of a multi instance deployment. OBSIDIAN_DATA_DIR is where the database file and the key live. OBSIDIAN_COOKIE_SECURE defaults to true, so cookies are only sent over HTTPS, and can be set to false for local HTTP testing. OBSIDIAN_TRUST_PROXY controls whether forwarded X-Forwarded-For headers are believed, and OBSIDIAN_TRUST_CLOUDFLARE should only be true when you are actually behind Cloudflare, because otherwise the CF-Connecting-IP header can be forged by any client. OBSIDIAN_PUBLIC_URL is the public address used in email verification links, OBSIDIAN_SMTP_HOST is the mail server for those messages, and OBSIDIAN_SSH_ADDR is the listen address for the terminal, for example :2222.

The container path is the same idea with a volume attached:

```bash
docker build -t obsidian-arc .
docker run -d -p 8080:8080 -v arc-data:/data \
  -e OBSIDIAN_SECRET_KEY=$(openssl rand -hex 32) \
  obsidian-arc
```

## The compose file binds to loopback on purpose

The docker-compose.yml in the repository is for the PostgreSQL variant, and its comments explain the design decisions in a way that is worth reading end to end before deploying it.

The SQLite default needs none of this, and the file is deliberately two services and nothing else: no reverse proxy, no cache, no queue, no metrics sidecar. The comment says to put your own TLS terminator in front if you need one.

POSTGRES_PASSWORD has no default on purpose, with the reasoning spelled out: a default written into a public file is a password everyone already knows. The file expects it in a .env beside it, and the documented start command is:

```bash
OBSIDIAN_SECRET_KEY=$(openssl rand -hex 32) docker compose up -d
```

The port publishing is the part that will save somebody from a bad afternoon. The service binds to 127.0.0.1 by default through OBSIDIAN_BIND, and the comment explains that this file expects a TLS terminator in front and that the environment tells the server to believe forwarded addresses, which is only safe while the port itself is not reachable. Publishing it on 0.0.0.0 with that setting on lets anyone forge their own address and walk past every rate limit. The escape hatch is spelled out too: set OBSIDIAN_BIND=0.0.0.0 and OBSIDIAN_TRUST_PROXY=false if you really want it exposed directly.

That is a well designed default, and it is also a warning that this project has a lot of security-relevant switches, and each of them has to be set deliberately for your own topology.

## Plugins are compiled in, and the repository ships none

The plugin system is unusual and is the one part of the design that needs reading twice.

Features that only some sites need exist as plugins, they live under plugins/, and they are compiled into the binary with build tags. If a plugin is not compiled, none of its code, routes, database tables or interface text is present at all. This repository ships no plugins of its own. A compiled-in plugin is then installed, enabled, disabled and uninstalled from the admin console like a browser extension, and disabling and uninstalling both require two-factor authentication, with the option to delete the data on uninstall. A plugin can also carry its own back-office sub-pages, and the architecture documentation for it lives at docs/architecture/plugins.md.

The Dockerfile takes the plugin set as a build argument, and the argument documentation says which plugins the binary carries, matching the names the Makefile uses, with the default being none. A deployment that wants plugins lays them over the tree and passes them in.

The build itself is three stages and one artefact, and the comments in the Dockerfile explain the shape. The frontend is compiled with Node and Vite into the directory the Go build embeds, the Go binary is built with CGO disabled, and the resulting image runs as a non-root user and contains no shell, no package manager and no interpreter, at a few tens of megabytes, which the comment gives as the reason the project has no Python in it.

Version stamping is handled in the Makefile, where the version is the git tag and nothing else. A build past the newest tag says how far past, and a tree with uncommitted changes ends in -dirty, so whatever a server reports is either a release you can check out again or plainly marked as not one.

## Conclusion

Adopt Obsidian Arc if you want to run a multi-user gateway to several model providers on your own hardware, with per-user quotas, provider routing and an admin terminal, and you are prepared to own the security settings around proxy trust and sign ups. Do not adopt it as a personal chat client, since most of the project is administration and policy. Verify first that your reverse proxy and the OBSIDIAN_TRUST_PROXY and OBSIDIAN_TRUST_CLOUDFLARE settings agree with each other, that the first registered account becomes the administrator as you expect, and that the secret key stays identical across instances if you run more than one.

## FAQ

### What does Obsidian Arc need to run?

A single Go binary with an embedded Vue 3 frontend, and SQLite or PostgreSQL for persistence. It requires no Redis, no message queue and no external cache, and it starts by default on port 8080 with its data in the ./data directory.

### Which model providers can Obsidian Arc talk to?

It is compatible with the Anthropic Messages API and with interfaces that speak the OpenAI protocol, naming OpenAI, DeepSeek, xAI, OpenRouter, Groq, Ollama and vLLM. A provider can host several models, with model mapping routing from an external model ID to the underlying model and aliases through an api_name.

### How are usage limits enforced in Obsidian Arc?

Every conversation and API call is recorded in a usage ledger, and limits apply over five hour, weekly and monthly periods on request counts, token consumption or credits. Database row locks are used so that concurrent requests cannot overspend the allowance.

### What can the Obsidian Arc terminal do?

It is a command line interface in the account menu, available to every account unless a user group disables it, and each person can only run what they could already do from the page. Administrators additionally get the user, group, model, health and usage commands, and with OBSIDIAN_SSH_ADDR set it is reachable over SSH as the terminal rather than a server shell.

### How do plugins work in Obsidian Arc?

Plugins live under plugins/ and are compiled into the binary with build tags, and a plugin that is not compiled leaves no code, routes, tables or interface text behind. Compiled plugins are installed, enabled, disabled and uninstalled from the admin console, and this repository ships none of its own.

## Sources

- [License: MIT](https://github.com/OnyxAxisOwO/ObsidianArc/blob/main/LICENSE)
- [OnyxAxisOwO/ObsidianArc on GitHub](https://github.com/OnyxAxisOwO/ObsidianArc)
- [Project website](https://obsidianarc.pages.dev)
- [README](https://github.com/OnyxAxisOwO/ObsidianArc/blob/main/README.md)
- [Releases](https://github.com/OnyxAxisOwO/ObsidianArc/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/onyxaxisowo-obsidianarc
