vcon-server

Add a second tenant

Make a second tenant over the admin API while the server runs, give it a key, see that its data and acme's stay apart, change its retention period, and remove it.

A tenant is one customer, department or application whose vCons are kept apart from every other tenant's. Each tenant has its own key, catalog, files, retention period and audit log. This how-to adds the tenant globex beside acme, over the admin API, with the server running.

CI runs every command on this page against a live server, after getting started, and checks each answer.

Before you start

  • A running server, set up as in getting started, with the tenant acme, and the certificate's copy for clients at /usr/local/share/vcon-server/ca.pem.
  • One shell for every step: later steps use variables that earlier steps set.
URL=https://vcon.example.com:8443
CONFIG=/etc/vcon-server/vcon-server.toml
ACME=$(sudo -u vcon vcon-server tenant list --config $CONFIG | jq -r 'select(.name == "acme") | .id')

The admin routes need an operator key, which only the server's host can make. Make one for this page:

OPS=$(sudo -u vcon vcon-server key create --operator --label second-tenant-ops --config $CONFIG | jq -r .token)

Make the tenant

POST /admin/tenants takes a name and, if you want one, a retention period. Without retention, the tenant takes the server's retention_default:

curl -sS -i --cacert /usr/local/share/vcon-server/ca.pem -X POST "$URL/admin/tenants" \
  -H "Authorization: Bearer $OPS" -H "Content-Type: application/json" \
  -d '{"name": "globex", "retention": "90d"}'
HTTP/2 201
content-type: application/json
content-length: 41
date: <date>

{"id":"<tenant id>"}

A name is unique on the server. A second globex is refused with name-taken:

curl -sS --cacert /usr/local/share/vcon-server/ca.pem -X POST "$URL/admin/tenants" \
  -H "Authorization: Bearer $OPS" -H "Content-Type: application/json" -d '{"name": "globex"}'
{"status":409,"title":"Another tenant has the name","type":"https://vcon-server.com/problems/name-taken"}

A member the route does not take, such as title, is refused with invalid-body, so a mistake is never ignored:

curl -sS --cacert /usr/local/share/vcon-server/ca.pem -X POST "$URL/admin/tenants" \
  -H "Authorization: Bearer $OPS" -H "Content-Type: application/json" -d '{"title": "initech"}'
{"status":422,"title":"The body is not one this route takes","type":"https://vcon-server.com/problems/invalid-body"}

List the tenants, by name:

curl -sS --cacert /usr/local/share/vcon-server/ca.pem "$URL/admin/tenants" -H "Authorization: Bearer $OPS" | jq -c '.[] | {name, retention}'
{"name":"acme","retention":null}
{"name":"globex","retention":"90d"}

retention null means the server default. Keep globex's id:

GLOBEX=$(curl -sS --cacert /usr/local/share/vcon-server/ca.pem "$URL/admin/tenants" -H "Authorization: Bearer $OPS" | jq -r '.[] | select(.name == "globex") | .id')

Give it a key

POST /admin/tenants/{id}/keys makes a tenant key over HTTP. The answer carries the token once, with Cache-Control: no-store:

curl -sS -i --cacert /usr/local/share/vcon-server/ca.pem -X POST "$URL/admin/tenants/$GLOBEX/keys" \
  -H "Authorization: Bearer $OPS" -H "Content-Type: application/json" \
  -d '{"scopes": ["write", "read"], "label": "globex-app"}' | tee globex-key.txt | grep -v '"token"'
HTTP/2 201
content-type: application/json
cache-control: no-store
content-length: 100
date: <date>

The last line of the answer, left out above, is {"id":"…","token":"vcs_…"}. Keep the token, and delete the file that held it:

GLOBEX_KEY=$(tail -n 1 globex-key.txt | jq -r .token)
rm globex-key.txt
curl -sS --cacert /usr/local/share/vcon-server/ca.pem "$URL/key" -H "Authorization: Bearer $GLOBEX_KEY" | jq -c --arg g "$GLOBEX" '{scopes, tenant: (.tenant == $g)}'
{"scopes":["write","read"],"tenant":true}

Its data and acme's stay apart

Make an acme key, and store one vCon with each tenant's key. The same uuid is stored under each tenant:

ACME_KEY=$(sudo -u vcon vcon-server key create --tenant "$ACME" --scope write --scope read --label second-tenant-acme \
  --config $CONFIG | jq -r .token)
UUID=$(cat /proc/sys/kernel/random/uuid)
printf '{"vcon":"0.4.0","uuid":"%s","created_at":"2026-10-27T12:00:00Z","parties":[{"tel":"+15555550103"}],"dialog":[],"attachments":[],"analysis":[]}' \
  "$UUID" > shared-uuid.vcon
for key in "$ACME_KEY" "$GLOBEX_KEY"; do
  curl -sS -o /dev/null -w '%{http_code}\n' --cacert /usr/local/share/vcon-server/ca.pem "$URL/vcons" \
    -H "Authorization: Bearer $key" -H "Content-Type: application/vcon" --data-binary @shared-uuid.vcon
done
201
201

Both answer 201. A uuid is unique within its tenant, so two tenants can each hold a vCon with the same uuid, and neither can tell. Each tenant's list holds only its own vCons:

curl -sS --cacert /usr/local/share/vcon-server/ca.pem "$URL/vcons" -H "Authorization: Bearer $GLOBEX_KEY" | jq '.vcons | length'
1

A vCon the globex key cannot see answers 404, whether it belongs to another tenant or does not exist:

OTHER=$(cat /proc/sys/kernel/random/uuid)
printf '{"vcon":"0.4.0","uuid":"%s","created_at":"2026-10-27T12:00:00Z","parties":[],"dialog":[],"attachments":[],"analysis":[]}' \
  "$OTHER" > acme-only.vcon
curl -sS -o /dev/null -w '%{http_code}\n' --cacert /usr/local/share/vcon-server/ca.pem "$URL/vcons" \
  -H "Authorization: Bearer $ACME_KEY" -H "Content-Type: application/vcon" --data-binary @acme-only.vcon
curl -sS -o /dev/null -w '%{http_code}\n' --cacert /usr/local/share/vcon-server/ca.pem "$URL/vcons/$OTHER" -H "Authorization: Bearer $GLOBEX_KEY"
curl -sS -o /dev/null -w '%{http_code}\n' --cacert /usr/local/share/vcon-server/ca.pem "$URL/vcons/$OTHER" -H "Authorization: Bearer $ACME_KEY"
201
404
200

Change its retention period

A tenant's retention period is how long a deleted vCon is kept, counted from when it was stored. PATCH /admin/tenants/{id} changes it, at the next purge sweep, for every version the tenant keeps:

curl -sS --cacert /usr/local/share/vcon-server/ca.pem -X PATCH "$URL/admin/tenants/$GLOBEX" \
  -H "Authorization: Bearer $OPS" -H "Content-Type: application/json" -d '{"retention": "1y"}' | jq -c '{retention}'
{"retention":"1y"}

{"retention": null} returns it to the server default. See retention and legal holds.

Remove it

DELETE /admin/tenants/{id} removes a tenant that keeps no version. Globex keeps one, so the server refuses with holds-vcons:

curl -sS --cacert /usr/local/share/vcon-server/ca.pem -X DELETE "$URL/admin/tenants/$GLOBEX" -H "Authorization: Bearer $OPS"
{"status":409,"title":"The tenant holds vCons; removing them needs force","type":"https://vcon-server.com/problems/holds-vcons"}

?force=true removes it with its vCons and revokes its keys in the same step. It refuses a tenant with a vCon under a legal hold, with under-hold:

curl -sS -o /dev/null -w '%{http_code}\n' --cacert /usr/local/share/vcon-server/ca.pem -X DELETE "$URL/admin/tenants/$GLOBEX?force=true" -H "Authorization: Bearer $OPS"
curl -sS -o /dev/null -w '%{http_code}\n' --cacert /usr/local/share/vcon-server/ca.pem "$URL/vcons" -H "Authorization: Bearer $GLOBEX_KEY"
204
401

The tenant's key is refused at once. Acme's copy of the shared uuid is untouched:

curl -sS -o /dev/null -w '%{http_code}\n' --cacert /usr/local/share/vcon-server/ca.pem "$URL/vcons/$UUID" -H "Authorization: Bearer $ACME_KEY"
200

Clean up

Revoke the two keys this page made on the host:

sudo -u vcon vcon-server key list --config $CONFIG | jq -r 'select(.label | startswith("second-tenant-")) | .id' \
  | while read -r id; do sudo -u vcon vcon-server key revoke "$id" --config $CONFIG; done
curl -sS -o /dev/null -w '%{http_code}\n' --cacert /usr/local/share/vcon-server/ca.pem "$URL/admin/tenants" -H "Authorization: Bearer $OPS"
401

The same with the command line

With the server stopped, vcon-server tenant create makes a tenant, as getting started does. With the server running or not, key create --tenant makes its keys and tenant set-retention changes its period. tenant remove runs only with the server stopped. See the command line.

Source of this page: website/content/docs/second-tenant.md