|
| 1 | +--- |
| 2 | +title: "Secret fields" |
| 3 | +metaTitle: "Encrypting secret custom fields and config args in Vendure" |
| 4 | +metaDescription: "Store sensitive values such as API keys encrypted at rest and control who can read them via the API using the `secret` flag." |
| 5 | +--- |
| 6 | + |
| 7 | +Some data stored by Vendure is sensitive — payment-gateway API keys, webhook signing secrets and |
| 8 | +similar. The `secret` flag lets you mark a custom field or a config arg so that its value is |
| 9 | +**encrypted in the database** and is **only returned in decrypted form via the API to users who are |
| 10 | +permitted to see it**. |
| 11 | + |
| 12 | +## Enabling encryption |
| 13 | + |
| 14 | +Encryption requires a stable secret key. Configure an [`EncryptionStrategy`](/reference/typescript-api/configuration/encryption-strategy/) |
| 15 | +in your VendureConfig, passing the secret key to its constructor: |
| 16 | + |
| 17 | +```ts |
| 18 | +import { DefaultEncryptionStrategy, VendureConfig } from '@vendure/core'; |
| 19 | + |
| 20 | +export const config: VendureConfig = { |
| 21 | + // ... |
| 22 | + systemOptions: { |
| 23 | + encryptionStrategy: new DefaultEncryptionStrategy({ |
| 24 | + secret: process.env.VENDURE_ENCRYPTION_KEY, |
| 25 | + }), |
| 26 | + }, |
| 27 | +}; |
| 28 | +``` |
| 29 | + |
| 30 | +```sh |
| 31 | +VENDURE_ENCRYPTION_KEY=your-long-random-secret |
| 32 | +``` |
| 33 | + |
| 34 | +:::warning |
| 35 | +The key must remain the same across restarts and deployments. If it changes, existing encrypted |
| 36 | +data can no longer be decrypted. Do not commit it to source control, and back it up securely. |
| 37 | +::: |
| 38 | + |
| 39 | +If a `secret` field or arg is configured but no encryption key is available, the server refuses to |
| 40 | +start, with a message showing exactly which configuration to add. |
| 41 | + |
| 42 | +## Marking a custom field as secret |
| 43 | + |
| 44 | +Set `secret: true` on a `string` or `text` custom field: |
| 45 | + |
| 46 | +```ts |
| 47 | +customFields: { |
| 48 | + Customer: [ |
| 49 | + { name: 'externalApiKey', type: 'string', secret: true }, |
| 50 | + ], |
| 51 | +}, |
| 52 | +``` |
| 53 | + |
| 54 | +The value is encrypted when saved and decrypted when loaded. `secret` cannot be combined with |
| 55 | +`unique` or an explicit `length`, and is not supported on `relation`, `struct` or list fields. |
| 56 | + |
| 57 | +## Marking a config arg as secret |
| 58 | + |
| 59 | +Set `secret: true` on a `string` config arg — for example the API key of a payment method handler: |
| 60 | + |
| 61 | +```ts |
| 62 | +args: { |
| 63 | + apiKey: { type: 'string', secret: true }, |
| 64 | +}, |
| 65 | +``` |
| 66 | + |
| 67 | +This works for any configurable operation — payment method handlers and eligibility checkers, |
| 68 | +shipping calculators and checkers, promotion conditions and actions, collection filters, and any |
| 69 | +custom `ConfigurableOperationDef` you define yourself. The encryption, redaction and |
| 70 | +preservation-on-edit behaviour is handled centrally, so a custom operation does not need to |
| 71 | +implement any of it. |
| 72 | + |
| 73 | +`secret` is only supported on non-list `string` args. Encryption turns the value into an opaque |
| 74 | +ciphertext string, so the arg has to be a `string`; list args are not supported because the |
| 75 | +redaction placeholder that preserves an unchanged secret on edit stands for a single value. |
| 76 | + |
| 77 | +## Who can read the value |
| 78 | + |
| 79 | +Reading the decrypted value requires the `Permission.ReadSecret` permission, which by default is |
| 80 | +held only by the SuperAdmin. A user without it receives a redaction placeholder instead of the real |
| 81 | +value. This behaviour is defined by the [`SecretAccessStrategy`](/reference/typescript-api/configuration/secret-access-strategy/), |
| 82 | +which you can replace to apply your own logic (for example, revealing a secret only to certain |
| 83 | +roles). Its input is a discriminated union on `kind`: for a `customField` it includes the entity |
| 84 | +instance, its type and the field name; for a `configArg` it includes the arg name and, where the |
| 85 | +GraphQL query path allows it to be determined, the owning entity type and field. |
| 86 | + |
| 87 | +Writing a secret is **not** gated by `ReadSecret` — any user who is allowed to create or update the |
| 88 | +entity may set its secret fields. |
| 89 | + |
| 90 | +## Editing without re-entering the secret |
| 91 | + |
| 92 | +Because the API never returns the real value to an unauthorised user, the placeholder is what their |
| 93 | +edit form has to submit back. When the placeholder is submitted on an update, the stored value is |
| 94 | +left unchanged. To change the value, submit a new one; to clear it, submit an empty value. On a |
| 95 | +_create_, the placeholder is rejected, since there is no existing value to preserve. |
| 96 | + |
| 97 | +## Moving data between environments |
| 98 | + |
| 99 | +Secret values are stored encrypted, so a database dump contains ciphertext, not the original values. |
| 100 | +The key that encrypted them lives only in your environment configuration, never in the database. This |
| 101 | +means a dump can only be read in an environment that uses the **same** encryption key. |
| 102 | + |
| 103 | +The first time an encryption key is used against a database, Vendure stores a small "key check" value |
| 104 | +(a known string encrypted with that key). On every startup it verifies the configured key against |
| 105 | +this check, so if you restore a dump into an environment with a different key — or change the key of |
| 106 | +an existing database — the server **fails to start** with a clear error, rather than failing later |
| 107 | +with scattered decryption errors when a secret is first read. |
| 108 | + |
| 109 | +As a result: |
| 110 | + |
| 111 | +- **To move data you want to keep usable** (for example promoting a staging database to production), |
| 112 | + use the **same `VENDURE_ENCRYPTION_KEY`** in both environments. |
| 113 | +- **To copy data into an environment that uses a different key** (for example pulling production data |
| 114 | + onto a developer machine without sharing the production key), first clear the secret values in the |
| 115 | + dump: set the encrypted custom-field columns to `NULL` and remove the secret config-arg values. |
| 116 | + Otherwise the restored data cannot be decrypted with the local key. |
| 117 | + |
| 118 | +Key rotation (re-encrypting existing data under a new key) is not currently supported. If you have |
| 119 | +intentionally changed the key and there is no encrypted data to preserve, delete the |
| 120 | +`vendure.encryption.keyCheck` entry from the settings store to re-bind the database to the new key. |
| 121 | + |
| 122 | +## Existing data and limitations |
| 123 | + |
| 124 | +- Enabling `secret` on a field that already holds plain-text values does not break anything, but |
| 125 | + those values are only encrypted the next time each record is saved. |
| 126 | +- Decrypted values are available to all internal code (events, jobs, plugins). The redaction applies |
| 127 | + only to the GraphQL API, so take care not to re-leak secrets in logs or exports. |
| 128 | +- `secret` fields cannot be filtered or sorted, because the stored value is ciphertext. |
| 129 | +- Key rotation is not supported in this version; changing the key makes existing data unreadable. |
0 commit comments