Skip to content

Integrations

Integrate Vault

Vault holds the fields you would rather not have in your own database: access tokens for a customer's systems, identity documents, anything a security questionnaire asks about. Each customer gets their own data encryption key, and every decrypt leaves a record.

Before you begin

  • A clear list of what belongs in Vault. Encrypting everything makes queries impossible; encrypt the fields whose exposure would be an incident.
  • A key scope decided up front. Per organization is the usual answer, because it lets one customer's data be destroyed without touching another's.
  • A place to store the returned object ID. Vault gives you an identifier; your row keeps that instead of the value.
  • A read path that runs on a server. A decrypt call is never made from a browser.

Install

Every example on this page uses the Node.js client. The same calls exist in the Python, Go, Ruby, PHP and Java SDKs, following the naming conventions of each language.

terminal
1npm i @paycux/node

Configure

Vault uses your ordinary API key. If a customer requires their own key material, the key reference below points at their KMS key rather than the platform-managed one.

.env.local
1PAYCUX_API_KEY=sk_example_7f4c1b9ad2e84c6f
2PAYCUX_VAULT_KEY_SCOPE=organization
3PAYCUX_VAULT_KEY_REF=paycux-managed

Then, in the dashboard:

  1. 1Open Vault and choose the key scope for the environment: one key per organization, or one key for the whole environment.
  2. 2Set the retention policy for objects, and whether a delete is immediate or held for a grace period.
  3. 3If a customer brings their own key, add their KMS key reference against their organization and confirm the grant works before you write anything under it.
  4. 4Turn on the audit stream for decrypt events. This is the record a security review will ask to see.
  5. 5Restrict which API keys may decrypt. Writing and reading are separate capabilities and most services only need one of them.

Implement

Store, read and delete. The value crosses the wire once on the way in and once on each read; in between, your database holds only an identifier.

Store a secret

Write the value against the organization it belongs to, then keep the returned object ID on your row. Metadata is not encrypted, so put searchable labels there and never the value itself.

lib/vault.ts
1import { Paycux } from "@paycux/node";
2
3const paycux = new Paycux(process.env.PAYCUX_API_KEY);
4
5export async function storeToken(organizationId: string, provider: string, value: string) {
6 const object = await paycux.vault.createObject({
7 organizationId,
8 name: provider + "-access-token",
9 value,
10 metadata: { provider, kind: "access_token" },
11 });
12
13 await db.integration.update({
14 where: { organizationId, provider },
15 data: { vaultObjectId: object.id },
16 });
17
18 return object.id;
19}

Read it back

Decrypt at the moment of use and let the value go out of scope immediately. Do not cache it, do not log it, and do not put it in an error message.

lib/vault-read.ts
1import { Paycux } from "@paycux/node";
2
3const paycux = new Paycux(process.env.PAYCUX_API_KEY);
4
5export async function withToken<T>(objectId: string, fn: (token: string) => Promise<T>) {
6 const object = await paycux.vault.readObject({
7 objectId,
8 reason: "outbound_api_call",
9 });
10
11 try {
12 return await fn(object.value);
13 } finally {
14 object.dispose();
15 }
16}

Rotate and destroy

Rotation writes a new version and leaves the old one unreadable; deletion removes the object. When a customer leaves, destroying their key makes every object under it unrecoverable in one step.

app/api/vault/rotate/route.ts
1import { Paycux } from "@paycux/node";
2import { requirePermission } from "@/lib/authz";
3
4const paycux = new Paycux(process.env.PAYCUX_API_KEY);
5
6export async function POST(request: Request) {
7 await requirePermission("vault:manage");
8 const { objectId, value } = await request.json();
9
10 const object = await paycux.vault.updateObject({ objectId, value });
11
12 return Response.json({ id: object.id, version: object.version });
13}

Verify

Two things need proving: that your own database no longer holds the plaintext, and that a decrypt is recorded.

  1. 1Store a value, then read the row directly out of your database. It must contain an object ID and no trace of the value.
  2. 2Read the value back through your server path and confirm it matches what you wrote.
  3. 3Check the audit stream and confirm the read appears with the actor, the object and the reason you passed.
  4. 4Attempt a decrypt with an API key that has no read capability and confirm it is refused.
  5. 5Rotate the object, then confirm the previous version can no longer be read while the current one can.

Reference

Request and response shapes for each of these live in the API reference. Paths are relative to https://api.paycux.com.

MethodPathWhat it does
POST/vault/objectsEncrypt and store a value, returning an object ID.
GET/vault/objects/:idDecrypt and return a stored value.
PUT/vault/objects/:idWrite a new version of an object.
DELETE/vault/objects/:idDestroy an object and every version of it.
GET/vault/objectsList objects by organization or metadata, without values.
GET/vault/keysList the data encryption keys in the environment.
POST/vault/keys/rotateRotate the key behind a scope, re-wrapping its objects.

Next steps