Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.envless.cloud/llms.txt

Use this file to discover all available pages before exploring further.

The Envless API server never sees plaintext and cannot encrypt for you. Before sending any variable value in a write request (PATCH, POST), you must encrypt it locally with your workspace key and pass the resulting ciphertext. This is the same e2e model the CLI and runtime use.

Use the @goenvless/sdk package

For any Node, Bun, Deno, or browser consumer, install the SDK and call encrypt() / decrypt() directly. No recipe to copy.
npm install @goenvless/sdk
import { encrypt, decrypt } from '@goenvless/sdk'

// Encrypt before sending to the API
const value = await encrypt(
    'postgres://user:pass@host/db',
    process.env.ENVLESS_PASSPHRASE!,
    workspaceId
)

await fetch('https://api.envless.cloud/projects/my-app/environments/production/variables/DATABASE_URL', {
    method: 'PATCH',
    headers: {
        Authorization: `Bearer ${process.env.ENVLESS_TOKEN}`,
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ value })
})

// Decrypt a value returned by the API
const plaintext = await decrypt(value, process.env.ENVLESS_PASSPHRASE!, workspaceId)
Both functions use the Web Crypto API directly — no native dependencies, no WASM, no polyfill required on Node 20+ / Bun / Deno / modern browsers.

Exports

ExportSignaturePurpose
encrypt(plaintext: string, passphrase: string, workspaceUniqueId: string) => Promise<string>Produce a v1:... ciphertext suitable for POST / PATCH
decrypt(ciphertext: string, passphrase: string, workspaceUniqueId: string) => Promise<string>Reverse of encrypt — accepts any v1:... value returned by the API
ENCRYPTION_PARAMETERSEncryptionParametersThe cryptographic constants — exposed so non-JS implementations can match the recipe byte-for-byte
EncryptionParameters (type)type of the constants objectUseful when wrapping the API in your own SDK

encrypt(plaintext, passphrase, workspaceUniqueId)

ParamTypeDescription
plaintextstringThe raw secret value to encrypt
passphrasestringWorkspace passphrase (same one used by envless passphrase set)
workspaceUniqueIdstringWorkspace UUID — returned by GET /workspace as id
Returns: Promise<string> — ciphertext in v1:<base64(iv || ciphertext)> format.

decrypt(ciphertext, passphrase, workspaceUniqueId)

ParamTypeDescription
ciphertextstringA v1:... string returned by the API
passphrasestringWorkspace passphrase
workspaceUniqueIdstringWorkspace UUID
Returns: Promise<string> — original plaintext. Throws: if the prefix is not v1:, the payload is malformed, or the passphrase / workspaceId don’t match the ones used to encrypt.

ENCRYPTION_PARAMETERS

const ENCRYPTION_PARAMETERS = {
    pbkdf2Iterations: 600_000,
    hash: 'SHA-256',
    aesLength: 256,
    ivLength: 12,
    saltLength: 16,
    saltNamespace: 'envless:workspace:',
    ciphertextVersionPrefix: 'v1:'
}
Use these when implementing the recipe in another language so your output decrypts cleanly with decrypt() here (and in the CLI, runtime, and dashboard).

Common pitfalls

SymptomCause
decryption failed on the dashboardWrong passphrase, wrong salt namespace, or missing v1: prefix
decryption failed in the CLIWrong workspaceUniqueId used for the salt
Validation passes locally but other tools can’t read itUsed a different IV length or skipped the IV in the ciphertext concatenation
Server returns 400Value not base64; or you sent plaintext by mistake
Never commit the passphrase to your repo. Pass it through an env var or your secret manager. Treat it with the same caution as the API key itself — anyone with the passphrase + key can decrypt every variable in the workspace.

The recipe (for non-JS implementations)

Use the package above for JavaScript / TypeScript. If you can’t, here’s the exact format — the tabs below show line-for-line equivalents in every popular language. Envless ciphertext follows one format end-to-end:
v1:<base64(iv || ciphertext)>
StepDetail
1. Derive the saltSHA-256("envless:workspace:" + workspaceUniqueId), take the first 16 bytes
2. Derive the keyPBKDF2(passphrase, salt, 600_000 iterations, SHA-256) → AES-GCM-256 key
3. EncryptAES-GCM with a 12-byte random IV per value
4. EncodeConcatenate iv ++ ciphertext, base64-encode, prefix with v1:
Algorithm constants:
ConstantValue
Key derivationPBKDF2
Iterations600,000
HashSHA-256
Symmetric cipherAES-GCM-256
IV length12 bytes
Salt length16 bytes
Salt namespaceenvless:workspace:
Ciphertext version prefixv1:
workspaceUniqueId is the workspace UUID — returned by GET /workspace as id. The passphrase is whatever you set with envless passphrase set (or in the dashboard).

Reference implementations

Pick your language. Every snippet below produces the same v1:... ciphertext that decrypt() in @goenvless/sdk (and the CLI, runtime, and dashboard) accepts identically.
Works in Node 20+, Bun, Deno, and browsers — all use the Web Crypto API. This is what @goenvless/sdk ships internally.
const PBKDF2_ITERATIONS = 600_000
const HASH = 'SHA-256'
const AES_LENGTH = 256
const IV_LENGTH = 12
const SALT_LENGTH = 16
const SALT_NAMESPACE = 'envless:workspace:'
const CIPHERTEXT_VERSION_PREFIX = 'v1:'

const encoder = new TextEncoder()

const toBase64 = (bytes: Uint8Array): string =>
    Buffer.from(bytes).toString('base64')

async function workspaceSalt(workspaceUniqueId: string): Promise<Uint8Array> {
    const digest = await crypto.subtle.digest(
        HASH,
        encoder.encode(`${SALT_NAMESPACE}${workspaceUniqueId}`)
    )
    return new Uint8Array(digest).slice(0, SALT_LENGTH)
}

async function deriveKey(passphrase: string, salt: Uint8Array): Promise<CryptoKey> {
    const baseKey = await crypto.subtle.importKey(
        'raw',
        encoder.encode(passphrase),
        { name: 'PBKDF2' },
        false,
        ['deriveKey']
    )

    return crypto.subtle.deriveKey(
        { name: 'PBKDF2', salt, iterations: PBKDF2_ITERATIONS, hash: HASH },
        baseKey,
        { name: 'AES-GCM', length: AES_LENGTH },
        true,
        ['encrypt', 'decrypt']
    )
}

export async function encrypt(plaintext: string, passphrase: string, workspaceUniqueId: string): Promise<string> {
    const salt = await workspaceSalt(workspaceUniqueId)
    const key = await deriveKey(passphrase, salt)

    const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH))
    const ciphertext = new Uint8Array(
        await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoder.encode(plaintext))
    )

    const combined = new Uint8Array(iv.length + ciphertext.length)
    combined.set(iv, 0)
    combined.set(ciphertext, iv.length)

    return `${CIPHERTEXT_VERSION_PREFIX}${toBase64(combined)}`
}