Blossom is a sing-box proxy subscription control plane. It is a TanStack Start SSR app built with React 19, backed by an oRPC API, better-auth authentication, and Drizzle/PostgreSQL. A Rust server-agent lives under server-agent/ and runs once per physical proxy server, consuming the /api/agent/* OpenAPI surface.
This repo is a pnpm workspace. Run everything from the repo root:
pnpm install
pnpm devpnpm devstarts the app dev server on http://localhost:3000.pnpm testruns the Vitest test suites in parallel across workspace packages.pnpm buildbuilds all workspace packages in parallel.
For app-only commands, use pnpm --filter @blossom/app <script>.
The app selects its deploy target through the DEPLOY_TARGET build switch in app/vite.config.ts. Valid values are cloudflare (default), netlify, vercel, and node. Because cloudflare is the default, plain pnpm build and pnpm dev behave exactly as before; platform-specific builds use pnpm build:<target>.
The
pnpm build:*scripts below are defined inapp/package.json. From the repo root, run them aspnpm --filter @blossom/app build:<target>.
| Platform | Build command | Config | How |
|---|---|---|---|
| Cloudflare Workers (primary) | pnpm build:cloudflare |
app/wrangler.jsonc |
cd app && wrangler deploy |
| Netlify | pnpm build:netlify |
netlify.toml |
Connect the repo, set base directory to app |
| Vercel | pnpm build:vercel |
app/vercel.json |
Connect the repo, set root directory to app |
| Railway | (Docker) | railway.toml |
Connect the repo; Railway builds from Dockerfile |
| Self-host Docker | (Docker) | Dockerfile, docker-compose.yaml |
docker compose up -d |
For Cloudflare, push secrets with wrangler secret put <NAME> and non-secret vars through vars in app/wrangler.jsonc. The Worker is bound to the blossom Hyperdrive configuration as HYPERDRIVE; when that binding is available, runtime database traffic uses Hyperdrive through node-postgres. DATABASE_URL remains the fallback for local/non-Cloudflare runtimes and is still required by Drizzle Kit migrations. For Netlify and Vercel, configure environment variables in their dashboards; the build presets handle the rest.
Every push to main (and v* tags) runs .github/workflows/docker.yml, which builds and publishes multi-arch (amd64/arm64) images to GHCR:
| Image | Contents |
|---|---|
ghcr.io/keiko233/blossom |
The app as a self-contained Node server on port 3000 (built with DEPLOY_TARGET=node). |
ghcr.io/keiko233/blossom/server-agent |
The Rust server-agent bundled with sing-box (compiled with with_v2ray_api). |
Tags: latest tracks main, sha-<commit> pins a build, and v* releases also get semver tags (1.2.3, 1.2). Note the app image only runs the server — database migrations still need to be applied separately (see below). VITE_APP_NAME is baked in at build time, so a custom app name requires building the image yourself with --build-arg VITE_APP_NAME=<name>.
Server env vars are validated at runtime on the first request, so production builds do not need secrets present at build time. Client env vars (VITE_*) are baked into the bundle and validated at build time.
| Variable | Required | Build / Runtime | Description |
|---|---|---|---|
DATABASE_URL |
Yes | Runtime | PostgreSQL connection string. |
DATABASE_DRIVER |
No | Runtime | neon-http or node-postgres. Auto-detected as neon-http for *.neon.tech URLs and node-postgres otherwise. |
BETTER_AUTH_URL |
Yes | Runtime | Public URL of the app, e.g. http://localhost:3000. |
BETTER_AUTH_SECRET |
Yes | Runtime | Secret key for better-auth. Generate with pnpm dlx @better-auth/cli secret. |
APP_NAME |
No | Runtime | Display name, defaults to Blossom. |
VITE_APP_NAME |
No | Build-time | Display name baked into the client bundle, defaults to Blossom. |
RESEND_API_KEY |
No | Runtime | Resend API key for email sending. |
RESEND_MAIL_FROM |
No | Runtime | Sender address used with Resend. |
GITHUB_CLIENT_ID |
No | Runtime | GitHub OAuth client ID. |
GITHUB_CLIENT_SECRET |
No | Runtime | GitHub OAuth client secret. |
GOOGLE_CLIENT_ID |
No | Runtime | Google OAuth client ID. |
GOOGLE_CLIENT_SECRET |
No | Runtime | Google OAuth client secret. |
Copy app/.env.example to app/.env.local for local development.
During local Cloudflare development, Vite supplies DATABASE_URL to Wrangler's local Hyperdrive binding automatically. This exercises the Hyperdrive connection path without remote pooling or caching; production Workers use the deployed Hyperdrive configuration.
Migrations live in app/drizzle/ and are applied with Drizzle Kit:
pnpm --filter @blossom/app db:migrateThe command reads DATABASE_URL from the environment. Run it manually or in CI before deploying. When using Docker Compose, a one-shot migrate service runs automatically before the app service starts.
The Docker Compose setup runs Postgres, applies migrations, and starts the app:
docker compose up -dThis brings up:
db— Postgres 17 on the internal network, persisted inpgdata.migrate— One-shot service that runspnpm db:migrateagainstdb.app— The Node server on http://localhost:3000, with a health check at/api/health.
Before any real deployment, change the placeholder BETTER_AUTH_SECRET and set BETTER_AUTH_URL to your public domain in docker-compose.yaml. VITE_APP_NAME is a Docker build ARG; rebuild the image to change the bundled client app name.
To skip building locally, point the app service at the prebuilt image (image: ghcr.io/keiko233/blossom:latest instead of build: .) — the migrate service still builds the builder stage locally, since migrations are not part of the runtime image.
The Rust server-agent in server-agent/ consumes the control-plane OpenAPI spec at /api/agent/*. Each server owns one agent token and one sing-box process; every enabled node assigned to that server becomes an inbound in the same generated sing-box config. New agents pull the versioned control document from /api/agent/config/v2, which carries server-managed polling intervals alongside the sing-box config. Candidates are checked by the target sing-box binary before reload, and the last-known-good config is persisted for rollback. After changing the agent-facing API, regenerate the agent client while the dev server is running:
pnpm --filter @blossom/app agent:specSee server-agent/.env.example for agent runtime configuration.
The app exposes a Model Context Protocol API at /api/mcp for external MCP clients. It allows authenticated administrators to manage users, nodes, servers, plans, and subscriptions through MCP tools.
MCP clients connect using OAuth 2.0 authorization code flow with PKCE. OAuth discovery is available at /.well-known/oauth-authorization-server; the issuer-path alias is /.well-known/oauth-authorization-server/api/auth, and protected-resource metadata is at /.well-known/oauth-protected-resource/api/mcp. After authorizing, the client receives an access token scoped to the MCP API.
| Scope | Permission |
|---|---|
blossom:mcp:read |
Read resources (users, nodes, servers, plans, subscriptions) and search/get sing-box documentation |
blossom:mcp:write |
Write operations (ban/unban users, set roles, create/update/delete nodes, update/delete servers, manage subscriptions) |
Write tools require explicit confirm: true and are recorded in the audit log.
Admin users can review and approve MCP client connections at /auth/mcp-consent. This endpoint lists requested scopes and allows the admin to grant or deny access.