Managing Connections
A connection is a configured instance of a connector — O2ID's
universal integration layer for external providers (SMS, email, external
identity providers, identity verification, and more). Every connector
type is registered in-process under a unique connectorType key; a
connection is a tenant's own named instance of one, holding that
connector's non-secret configuration plaintext and its secret fields
(API keys, OAuth client secrets, ...) encrypted at rest.
This release ships the connections foundation — the generic data model, encryption, and CRUD API — with no concrete connector types registered yet. Concrete categories (external identity providers for federated login, SMS/email notifications, identity verification) are built on top of this foundation in later releases; each adds its own connector package and capability interface without changing the API described here.
Managing connections requires a role with the connections:read/
connections:create/connections:update/connections:delete scopes —
see Managing Roles for how to create and assign
roles.
Encryption at rest
Connection secrets are stored reversibly (not hashed, unlike user passwords and application client secrets) — a connector needs the plaintext to call the third-party service it wraps. They're encrypted with AES-256-GCM under a key derived per tenant (via HKDF, keyed by tenant ID) from a single root key, so no per-tenant key material is ever stored — a leak of one tenant's data can't be combined with another tenant's.
This is a defense-in-depth measure for this one sensitive field, not a replacement for encrypting your storage layer as a whole — that's a deployment responsibility covered in Storage.
Field-level encryption here specifically protects against someone who can query the live, running database (a SQL-injection read, an over-privileged replica, a support engineer debugging a ticket) from reading third-party API keys and OAuth client secrets in plaintext; encrypting the database's underlying storage protects everything else against a stolen disk or an unencrypted backup. Configure both.
The root key is a 256-bit value (hex- or base64-encoded), read from an environment variable at startup:
export O2ID_MASTER_KEY=$(openssl rand -hex 32)
o2id serve
The env var name is configurable via secrets.master_key_env in
o2id.toml (defaults to O2ID_MASTER_KEY). O2ID fails to start if the
key is missing or malformed — there's no fallback, since serving with a
misconfigured key would mean no connection secret could be decrypted at
all.
Discovering available connector types
curl -s http://localhost:8080/t/system/connections/meta \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
Each entry describes a connector type's type key, category, and
configSchema — the field names, types, and which are secret — so a
caller (or a future admin UI) knows what to pass when creating a
connection of that type, without hardcoding per-connector knowledge.
Creating a connection
curl -s -X POST http://localhost:8080/t/system/connections \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"connectorType": "<a type from /connections/meta>",
"name": "My Connection",
"fields": {
"<configField>": "value",
"<secretField>": "secret-value"
}
}'
fields carries both non-secret configuration and secret values in one
map, keyed by the connector type's configSchema field keys — the
service splits them apart before persisting, encrypting only the ones
marked secret. Every field the connector type marks required must be
present or the request is rejected with 400.
The response never includes secret values — only secretKeys, the list
of which secret fields are currently set:
{
"id": "9f8e7d6c5b4a3928170f6e5d4c3b2a19",
"connectorType": "...",
"category": "...",
"name": "My Connection",
"config": { "...": "..." },
"secretKeys": ["apiKey"],
"enabled": true,
"createdAt": "...",
"updatedAt": "..."
}
Updating a connection
curl -s -X PATCH http://localhost:8080/t/system/connections/<id> \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"fields": {"apiKey": "rotated-value"}}'
fields on update is merged key-by-key into the connection's
existing field set, not replaced wholesale — since the API never echoes
back current secret values, a client has no way to resupply every
credential just to rotate one of them or rename the connection. name
and enabled are likewise only changed when explicitly supplied.
Testing a connection
curl -s -X POST http://localhost:8080/t/system/connections/<id>/test \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
Resolves the connection and invokes the connector's own live validation (e.g. an authenticated no-op call to the provider) without persisting anything:
{"valid": false, "error": "invalid credentials"}
A failed test is reported as 200 with valid: false — a bad API key
is an expected outcome of testing a connection, not a server error.
Listing and deleting
curl -s http://localhost:8080/t/system/connections \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
curl -s -X DELETE http://localhost:8080/t/system/connections/<id> \
-H "Authorization: Bearer ${ACCESS_TOKEN}"
GET /connections lists every connection for the tenant (with secrets
masked the same way as a single GET); DELETE /connections/{id}
removes one.
Troubleshooting
| Situation | Response |
|---|---|
name missing or empty | 400 — invalid connection name |
connectorType names a type nothing has registered | 400 — unknown connector type |
A required field (per the connector type's configSchema) is missing | 400 — missing required field |
name already used by another connection in the tenant | 409 — connection name already exists |
<id> doesn't match anything | 404 — connection not found |
O2ID_MASTER_KEY missing or malformed at startup | O2ID exits immediately with an error — connections can't be encrypted or decrypted without it |
API Reference
See the API Reference for the full request/response schemas.