Self-Hosting the Team Server
Deploy the Vrge collaboration server on your own infrastructure. This guide is for Team license customers who need real-time collaboration across their organization.
What the server does
The server is the sync layer for shared entities: clients, projects, tasks, invoices, comments, attachments. The AI observer pipeline (email, calendar, files, bank) runs locally on each teammate's machine and keeps their proposal queue private. When a teammate accepts a proposal, the resulting entity syncs to every other client over WebSockets. AI provider keys and raw observer content never touch the server.
Mailbox and calendar connections are the one exception, and the reason is admin control: each teammate's Gmail, Outlook and Calendar grants are stored on your own server, encrypted at rest with SERVER_TOKEN_ENCRYPTION_KEY, so an admin can revoke a departing employee's access centrally. They stay on your infrastructure — Vrge never sees them — and the scopes they unlock are read-only.
One license for your whole organization
Your Team license activates once on the server, not on each employee's machine. Your team joins by email invite and never enters a license key. No per-seat activation, no license management overhead, no SaaS-style gotchas.
1. How activation works
Five steps, end to end. The admin does the first three. Everyone else just opens an email.
- Admin deploys the team server (Docker or Node, covered below).
- Admin activates the org license on the server, once, using the Lemon Squeezy key from their purchase email.
- Admin invites employees from the admin panel. The server emails each invite once you have given it a mail provider (section 5).
- Employees click the invite link and set a password in the browser. No install required.
- Optional: any employee can download the desktop app and point it at the team server URL for offline access and native integrations.
2. Prerequisites
- Docker and Docker Compose installed on your server
- Alternatively, Node.js 18+ if running without Docker
- A Team license key: purchase at getvrge.com/pricing. The server download comes with it.
- A server with at least 1 GB RAM and 1 CPU core (any Linux VPS, on-premises machine, or cloud instance)
3. Quick Start with Docker
You build the server image yourself
The server ships as a tarball attached to your Team purchase — the download link is in your Lemon Squeezy purchase email and on your order page. Inside are the server source, the web client, an .env.example and a docker-compose.yml. Docker builds the images on your own machine: there is nothing to pull from a registry, and nothing to trust that you haven't compiled yourself.
That is deliberate. You can read every line that ends up running on your server, which is a stronger guarantee than a signature from us. It also means no published namespace for anyone to squat.
tar -xzf vrge-team-server-*.tar.gz
cd vrge-team-server-*
cp .env.example .env
# fill in the secrets below, then:
docker compose up -d --buildThen open http://localhost:8080. The first user to register becomes the org admin. The first run compiles the web client from source, so expect a few minutes; later runs reuse the Docker cache.
Generating your secrets
The server needs three secrets, and .env.example carries no real values for them — what is there is placeholder text. Generate them before the first boot; a later duplicate of the same key in a .env file wins, so appending is enough:
echo "JWT_SECRET=$(openssl rand -hex 32)" >> .env
echo "CSRF_SECRET=$(openssl rand -hex 32)" >> .env
echo "SERVER_TOKEN_ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .envEach is a 64-character hex string (256 bits of entropy). Keep them backed up somewhere safe. See the Environment Variables section for what they do and the consequences of losing them.
LICENSE_KEY is placeholder text in .env.example as well. Replace it with the key from your purchase email: Compose only stops you when the value is empty, so a leftover placeholder starts a server whose licence key was never bought (section 7).
4. Environment Variables
| Variable | Default | Description |
|---|---|---|
API_PORT | 3001 | Server API port |
WEB_PORT | 8080 | Web UI port |
CORS_ORIGIN | http://localhost:8080 | Browser origin allowed to call the API. Must match the URL you actually open Vrge at, scheme and port included |
APP_URL | http://localhost:8080 | Public URL of this deployment. Invite emails link to it, so it has to be a URL your team can open — the default is not |
REQUIRE_HTTPS | 1 | On by default: plaintext requests are refused with 426 Upgrade Required. Requests from the server itself are exempt, so a local trial needs no change. Set 0 only for a deliberate plaintext deployment on a network you trust |
TRUST_PROXY | 1 | Trust the first proxy hop for X-Forwarded-* headers. On by default because the bundled nginx is the only thing that reaches the server |
JWT_SECRET | (required) | Signs login tokens (JWTs) and the CSRF tokens that pair with them. Minimum 32 characters; 64 hex chars recommended |
CSRF_SECRET | (required) | Signs the anti-forgery tokens that protect form submissions. The Docker compose file will not start without it. 64 hex chars recommended |
SERVER_TOKEN_ENCRYPTION_KEY | (required) | AES-256-GCM key for encrypting OAuth tokens (Gmail, Outlook, Calendar) at rest. Must be exactly 64 hex characters |
DATABASE_PATH | /app/data/vrge.db | SQLite database file path |
LICENSE_KEY | (required) | The Team license key from your purchase email. Compose refuses to start without it, and the server prefers it at boot over a key activated through the admin UI |
LICENSE_TIER | team-starter | The tier that key was bought at: solo, team-starter, team-studio or team-agency. Only read when LICENSE_KEY is also set. See the note below, because it is checked |
LICENSE_SEATS | (optional) | Seats agreed above your tier's included number, for organisations larger than Team Agency's 40. Available by arrangement — email us first. It can only RAISE the cap, never lower it, and is ignored on an unlicensed server. The effective cap is printed in the boot log so you can confirm it without inviting anyone |
scroll →
Changing WEB_PORT does not move CORS_ORIGIN or APP_URL with it. Set all three when you change the port or put a domain in front.
Two ways to license the server, and which one wins
On the Docker stack these two variables are the licence: Compose refuses to start without LICENSE_KEY, and the server reads it at boot in preference to anything activated later through the admin UI. Pasting a key into Settings → Workspace → License (section 7) is the route for a server run outside Docker, where these are optional.
Either way the key is confirmed with Lemon Squeezy, and the tier you declared is checked against what was actually purchased — corrected if they disagree, with a warning in the log so you can fix the setting. If the server cannot reach Lemon Squeezy (no network, an airgapped install) what you declared stands, so an offline server keeps working.
Consequences of losing these secrets
- JWT_SECRET: if you rotate it, every active session is invalidated and users must sign back in. No data loss.
- SERVER_TOKEN_ENCRYPTION_KEY: if you rotate or lose this key, every employee's stored Gmail/Outlook/Calendar OAuth grants become unreadable. Employees re-authenticate via Settings → Sources to restore access. No CRM data is lost, but mailbox connections need to be redone.
- Back up both keys alongside your database backup. The encrypted OAuth rows in your SQLite file are useless without the matching key.
5. Email & Payment Links
Two more server capabilities switch on when their environment variables are set. Both credentials live only on your server — the desktop app never holds them, and they are unrelated to the read-only bank-feed connections teammates set up in the app.
Outbound email: invites, invoices, notifications
Set this up before you invite anyone. Email is how the server delivers an invite link; without it there is nothing to deliver one with. Configuring it also gives the invoice editor a working Send Invoice button that emails the invoice straight to the client. Vrge supports two delivery paths — set one:
| Variable | Default | Description |
|---|---|---|
RESEND_API_KEY | (unset) | API key from resend.com — the simpler path. If both paths are configured, Resend wins |
SMTP_HOST | (unset) | SMTP server hostname — the bring-your-own-mailserver path. Pair with SMTP_PORT (default 587), SMTP_USER and SMTP_PASS |
EMAIL_FROM | noreply@example.com | From address on everything the server sends. Set it — the default is a placeholder that most mail providers will reject |
EMAIL_FROM_NAME | Vrge | Display name on the From line — usually your company name |
scroll →
Verify it from Settings → Email delivery → Send a test email as the admin. Invoice sending stays hidden until delivery is set up.
Invite links are built from APP_URL
The join link inside an invite email is APP_URL plus the token, so set it to the URL your team actually opens before you send the first invite. The shipped default is http://localhost:8080, which resolves to the recipient's own machine — the mail goes out, the admin panel reports it sent, and the link opens nothing.
Invoice payment links (Stripe)
With a Stripe key on the server, every unpaid invoice gains a Generate Payment Link button. The server creates a Stripe Checkout page for the invoice amount (charged in USD), saves the link on the invoice for copying, and includes it in the invoice email. If you also connect the webhook, the loop closes itself: when the client pays, Stripe notifies your server and the invoice flips to paid on every teammate's screen — no bookkeeping click.
| Variable | Default | Description |
|---|---|---|
STRIPE_SECRET_KEY | (unset) | Secret key from your Stripe dashboard. Setting it is what turns the feature on |
STRIPE_WEBHOOK_SECRET | (unset) | Signing secret for the webhook below. Without it, payments don't auto-mark invoices paid — links still work |
STRIPE_SUCCESS_URL | (localhost placeholder) | Where the client's browser lands after paying — a thank-you page you host, or just your website. Set it in production |
STRIPE_CANCEL_URL | (localhost placeholder) | Where the client lands if they back out of checkout |
scroll →
Webhook setup (for the auto-paid flip): in the Stripe dashboard, add a webhook endpoint pointing at https://your-server/api/webhooks/stripe subscribed to the checkout.session.completed event, then put its signing secret in STRIPE_WEBHOOK_SECRET. The server verifies every delivery's signature and rejects stale or unsigned calls. If you skip the webhook, mark invoices paid yourself — or let a connected bank feed propose the match when the payout lands.
This is the one write-capable key in the system
A Stripe secret key can create charges, so treat it like the other server secrets: environment variable only, never in a client, never in git. It is deliberately separate from the Stripe connection teammates make inside the app (Settings → Sources), which is a read-only bank feed and cannot move money. The desktop app holds no write access to anything — payment links exist only where you, the admin, explicitly configured them on your own server.
6. Database & Data Persistence
- Vrge uses SQLite. No external database server required
- Data is stored in a Docker volume called
vrge-dataat the path specified byDATABASE_PATH - Attachments are written to disk rather than into the database, in a second volume called
vrge-uploads - Both volumes persist across container restarts and updates
Backing up your server
Backing up the server is your job, and nothing in the app does it for you. Vrge's in-app backup controls — Download Backup, automatic folder backups, and Vrge Cloud Backup — all act on the local database of a soloinstall. In team mode your records live in this server's database, so the app hides those controls rather than write a file that would not contain your team's work. Hosted backup for team servers is planned after launch.
Stop the server first. SQLite is mid-write on a running server, and a hot copy can capture a torn file that restores into corruption. The downtime is a few seconds.
docker compose stop server
docker compose cp server:/app/data/vrge.db ./vrge-$(date +%F).db
docker compose cp server:/app/uploads ./vrge-uploads-$(date +%F)
docker compose start serverRestore the same way, with the server stopped. Migrations run on boot, so an older database restored onto a newer server is migrated forward; the reverse will not boot. Two things make a backup useless if you skip them: the team_user_oauth_tokens rows are encrypted with SERVER_TOKEN_ENCRYPTION_KEY, so back up your .env alongside the database — either is useless without the other — and the attachments volume is separate from the database, so a database-only backup restores rows pointing at files that no longer exist. Schedule it with cron, and test a restore before you need one.
7. Activate the Org License
Your Team license is activated on the server, once, not on anyone's laptop. Nobody else on your team will ever see or enter a license key.
On the Docker stack: set it in .env
Compose will not start without LICENSE_KEY, and the server reads it at boot ahead of anything stored later, so on a Docker deployment this is the licence:
LICENSE_KEY=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
LICENSE_TIER=team-starterThe key is the UUID from your purchase email. You do not have to get the tier right: the server confirms the key with Lemon Squeezy in the background and corrects the tier if you declared the wrong one, logging a warning so you can fix the setting. The seat count it settled on is printed in the boot log.
Outside Docker: paste it into the admin UI
Where LICENSE_KEY is not set, sign in as the admin, go to Settings → Workspace → License, paste the key and click Activate. It is verified with Lemon Squeezy and stored in the database. On the Docker stack LICENSE_KEY wins back at the next restart, so change .env there rather than the UI.
What happens with no network
Verifying a key is one outbound HTTPS call to api.lemonsqueezy.com. There is no API key or secret of ours to install for it, and if the server cannot reach Lemon Squeezy the answer is “we could not ask”, never “your key is bad”. The admin UI refuses the paste and changes nothing; a server licensed through .env keeps exactly the tier and seat count it already had. An airgapped install runs on what you declared, indefinitely.
8. Invite Your Team
Once the org license is active and email delivery is set up (section 5), send invites from the admin panel.
- Go to Settings → Team → Invite
- Enter the employee's email and role (admin or member)
- Click Send invite. The server emails them a signed, single-use join link
Invite links expire after 7 days. You can revoke an invite from the same panel; if someone never received theirs or let it lapse, revoke it and send a fresh invite.
How many people your license covers — and yes, you count
A license is counted in people with an account on your server, and the admin account created when you first registered is one of them. Team Starter is you plus four colleagues, not you plus five.
Computers are not counted. One person using the desktop app on two machines and a browser on a third is still one person.
| License | LICENSE_TIER value | People, including you |
|---|---|---|
| Solo | solo | 1 |
| Team Starter | team-starter | 5 |
| Team Studio | team-studio | 15 |
| Team Agency | team-agency | 40 |
scroll →
When every place is taken, the next invite (and the next signup against it) is refused with USER_LIMIT_REACHED rather than quietly failing. The check runs in the same database transaction that creates the account, so two people accepting invites at the same moment cannot both slip through. Free a place up by removing someone who has left, or move up a tier and update your license. Need more than 40? Get in touch.
9. How Employees Join
Employees need nothing but their invite email to get working.
- Open the invite email and click Join your team
- The link opens the team server in a browser. Set a password, you're in
- Work from the browser on any device: no install, no license key
Optional: desktop app for offline + native integrations
Any employee can also download the Vrge desktop app, enter the team server URL and their email + password, and unlock offline support, local AI (Ollama), and native OS integrations. The org license already covers them. The desktop app never prompts for a key.
10. Permissions & Audit Log
Two team-mode behaviours worth knowing about as an admin, both designed to make multi-user collaboration safer without putting you in the middle of every change.
Who can change what
Vrge uses flat collaboration: the team shares one workspace, and the only hard line is around deleting.
- Everyone can read every shared entity (the team is small enough that segmentation adds friction without much benefit)
- Any member can edit any client, project, task, note, invoice or expense. Vrge records who made each change in the activity log rather than blocking the edit. The assumption is that a small team wants to fix each other's records, not queue up behind whoever created them.
- Only admins can delete shared entities. The destructive path stays narrow on purpose. A misclick, a departing employee or a compromised account should not be able to wipe a coworker's project. Members can still delete their own comments and uploads.
- Viewers cannot change anything. Assign the viewer role to anyone who should only read.
Activity log (Settings → Activity)
Every create, edit, and delete is recorded with the actor, entity, action, and timestamp. Filter by user, entity type, action, or date range. Useful for:
- Investigating "who changed this client on Tuesday?"
- Compliance evidence in regulated verticals (legal, medical, financial)
- Recovering after an accidental edit: the audit row points at the entity even if it's since been deleted
Logs are retained for the life of the database. There's no automatic pruning. Capture them in your regular SQLite backups.
Sessions & sign-in
Vrge uses short-lived (15 minute) access tokens that silently refresh from a 7-day rotating cookie. End-users see this as "stays signed in". They don't hit a re-login screen unless they've been away for more than a week. If a refresh token is ever reused after rotation, the entire session family is revoked automatically and that user is forced to sign in again on all devices: Vrge's built-in defence against stolen-cookie replay attacks.
11. Updating
New versions arrive as a new tarball on the same Lemon Squeezy download link. There is no docker compose pull step: these images are built from the source in the tarball, not pulled from a registry.
tar -xzf vrge-team-server-<new>.tar.gz
cd vrge-team-server-<new>
cp ../vrge-team-server-<old>/.env .
docker compose up -d --buildYour data lives in the vrge-data and vrge-uploads Docker volumes, not in the folder you extracted, so it survives the swap. Migrations run on boot. Back up first anyway.
Upgrading from a build before 6 August 2026: attachments used to be written into the container's own filesystem rather than a volume, so rebuilding discarded them. Rescue them while the old container still exists, before your first up --build on the new version, then copy them into the new volume afterwards. Databases were always on a volume and were never affected.
docker compose cp server:/app/uploads ./uploads-rescue
docker compose up -d --build
docker compose cp ./uploads-rescue/. server:/app/uploads12. Security Recommendations
- Use HTTPS: place Vrge behind a reverse proxy (e.g., Nginx, Caddy, or Traefik) with TLS certificates, then set
REQUIRE_HTTPS=1so the server refuses plaintext. Your proxy has to setX-Forwarded-Proto— Caddy and Traefik do by default; for nginx addproxy_set_header X-Forwarded-Proto $scheme; - Use 256-bit secrets for both keys: generate with
openssl rand -hex 32. The encryption key requires exactly 64 hex characters; the JWT secret requires at least 32. - Rotate JWT_SECRET annually, or after any suspected compromise. Rotation invalidates every active session, so users will need to log back in. Do this during a low-traffic window.
- Rotate SERVER_TOKEN_ENCRYPTION_KEY with care: rotation makes existing OAuth ciphertext unreadable. Employees will be prompted to reconnect their Gmail/Outlook/ Calendar grants. No CRM data is affected. Annual rotation is a reasonable default.
- Restrict network access: only expose the server on your internal network or behind a VPN if possible
- Keep Docker updated: regularly update Docker and your host OS to patch security vulnerabilities
- Back up regularly: schedule automated backups of the SQLite database file
- Monitor logs: check container logs with
docker compose logs -ffor any issues