Typed clients with nothing hidden behind them

TypeScript with zero dependencies, Python with sync and async clients, Ruby with RBS types, PHP with PHPStan and Psalm types, and 113 methods in each covering all 80 API operations, enforced by parity checks in CI.

What ships in the package

Typed TypeScript, Python, Ruby and PHP clients for the Envless public API, with thirteen resource namespaces and the dashboard's encryption primitives, so you can encrypt a value locally before writing it.

Full definition

The @goenvless/sdk package is a typed TypeScript client for the Envless public API. It exposes thirteen resource namespaces, three pagination shapes on every collection, two error classes you can branch on without parsing messages, and the same client-side encryption primitives the dashboard uses, so you can encrypt a value locally before writing it through the API. It is a separate concern from @goenvless/env, the runtime package that reads variables into a running application: the SDK manages Envless, the runtime consumes it. Its Python twin, envless-sdk on PyPI, has the same 113 methods under snake_case names, with a synchronous and an asynchronous client. Its Ruby twin, envless-sdk on RubyGems, has them too, with RBS types and no dependencies, and so does its PHP twin, envless/sdk on Packagist, with PHPStan and Psalm types and no Composer dependencies. All of them send the same requests and write the same ciphertext.

  • Methods covering API operations

    113 / 80

    Every documented operation has a method with a matching operationId and declared scope. A sdk:parity script diffs the manifest against the OpenAPI document and exits non-zero on any drift, so the coverage claim is enforced rather than asserted.

  • Runtime dependencies

    0

    The TypeScript package has no dependencies key at all. The built bundle imports nothing external and touches no Node built-ins, using only fetch, crypto.subtle and base64 helpers. Dual ESM and CommonJS with separate type declarations, sideEffects false, Node 20 or newer.

  • Resource namespaces

    13

    me, workspace, workspaces, products, projects, environments, variables, versions, members, invites, roles, keys and webhooks. Each collection ships list, iterate and listAll.

  • The same client on PyPI

    Python

    envless-sdk has every method under its snake_case name, with Envless for synchronous code and AsyncEnvless for asyncio and trio. A parity check replays all 113 methods through both Python clients against the requests the TypeScript SDK records and fails on any difference.

  • The same client on RubyGems

    Ruby

    envless-sdk has every method under its snake_case name, RBS signatures for every request and response, and no runtime dependencies. Its own parity check replays all 113 methods against the requests the TypeScript SDK records and fails on any difference.

  • The same client on Packagist

    PHP

    envless/sdk has every method under its TypeScript name, a PHPStan and Psalm array shape for every request and response, and no Composer dependencies. Its own parity check replays all 113 methods against the requests the TypeScript SDK records and fails on any difference.

  • The only retried statuses

    408, 429, 5xx

    Retries fire on GET, HEAD, PUT and DELETE only, with exponential backoff of 2^attempt times 250ms capped at 30 seconds. POST and PATCH are never retried, so a create cannot be duplicated by a timeout.

  • Default webhook tolerance

    300s

    verifyWebhookSignature implements Standard Webhooks: HMAC-SHA256 over id, timestamp and the raw payload, compared in constant time, accepting the multi-signature header format.

Using it

  1. 1

    Install and initialise

    Call init once at startup and import the shared client anywhere, or construct explicit clients when you talk to more than one workspace. The constructor throws if no token is present, and createClient falls back to ENVLESS_TOKEN.

    npm install @goenvless/sdk
    
    import { init, envless } from '@goenvless/sdk'
    
    init({ token: process.env.ENVLESS_TOKEN })
    
    const identity = await envless.me.get()
  2. 2

    Page through collections three ways

    list returns one page with a pagination envelope, iterate is an async generator that walks every page, and listAll collects them. Both walking shapes clamp the per-request limit to 100, which is also the API ceiling.

    const page = await envless.projects.list({ limit: 50, offset: 0, search: 'api' })
    
    for await (const project of envless.projects.iterate()) console.log(project.slug)
    
    const all = await envless.projects.listAll()
  3. 3

    Branch on codes, never on messages

    EnvlessApiError carries the status, a stable machine-readable code, the resource, the field, a request id and a retry hint, plus convenience getters for the cases you actually handle. EnvlessNetworkError carries the original cause.

    import { EnvlessApiError, EnvlessNetworkError } from '@goenvless/sdk'
    
    try {
        await envless.projects.create({ name: 'Billing', slug: 'billing' })
    }
    catch (error) {
        if (error instanceof EnvlessApiError) {
            if (error.isScopeMissing) console.error('key needs scope:', error.resource)
            else if (error.needsUpgrade) console.error('plan limit:', error.message)
            else console.error(error.code, error.status, error.requestId)
        }
        else if (error instanceof EnvlessNetworkError) console.error('unreachable')
        else throw error
    }
  4. 4

    Encrypt before you write

    The API refuses any value that is not already an envelope, so writing a variable through the SDK means encrypting it locally first with the same primitives the dashboard uses.

    import { encryptValue, envless } from '@goenvless/sdk'
    
    const { workspaceId } = await envless.me.get()
    
    const value = await encryptValue(
        'postgres://user:pass@db.internal:5432/app',
        process.env.ENVLESS_PASSPHRASE,
        workspaceId
    )
    
    await envless.variables.create('api', 'production', { name: 'DATABASE_URL', value })
  5. 5

    Verify webhooks against the raw body

    Pass the exact bytes you received, not a re-serialised object, or the signature will not match. The helper checks the timestamp against the tolerance window and compares in constant time.

    import { verifyWebhookSignature } from '@goenvless/sdk'
    
    const event = await verifyWebhookSignature({
        payload: await readRawBody(req),
        headers: req.headers,
        secret: process.env.ENVLESS_WEBHOOK_SECRET,
        toleranceSeconds: 300
    })
  6. 6

    Or do all of it from Python

    envless-sdk is the same client for Python: the same methods in snake_case, request bodies and results as typed dicts, and AsyncEnvless with every method awaited for asyncio and trio.

    pip install envless-sdk
    
    from envless import envless
    
    identity = envless.me.get()
    
    for project in envless.projects.iterate():
        print(identity['workspaceId'], project['slug'])
  7. 7

    Or from Ruby

    envless-sdk is the same client for Ruby: the same methods in snake_case, request bodies as keyword arguments, results as Hashes with Symbol keys, and RBS types that Steep checks.

    bundle add envless-sdk
    
    require "envless/sdk"
    
    identity = Envless.me.get
    
    Envless.projects.iterate do |project|
      puts "#{identity[:workspaceId]} #{project[:slug]}"
    end
  8. 8

    Or from PHP

    envless/sdk is the same client for PHP: the same method names, request bodies and results as arrays keyed by the API field names, options as named arguments, and PHPStan and Psalm types for every call.

    composer require envless/sdk
    
    use Envless\Envless;
    
    $envless = new Envless();
    $identity = $envless->me->get();
    
    foreach ($envless->projects->iterate() as $project) {
        echo $identity['workspaceId'], ' ', $project['slug'], PHP_EOL;
    }

The SDKs FAQ

The SDK manages Envless: it creates projects, writes variables, rotates keys and reads audit data through the public API with an API key. The env package consumes Envless: it loads the variables for one project and environment into a running application and exposes them as a typed, read-only proxy. An application typically uses env; a script, an internal tool or an integration uses the SDK.

The TypeScript SDK refuses to construct in one unless you pass dangerouslyAllowBrowser, and the flag is named that way on purpose. Beyond the obvious problem of shipping an API key to users, the API only echoes the dashboard origin in its CORS headers, so a browser request would be blocked anyway. Use the SDK from a server.

Because a timeout does not tell you whether the write landed. Retrying a create after a request that may have succeeded is how you end up with two of something. GET, HEAD, PUT and DELETE are idempotent, so they retry on 408, 429 and the 5xx family with exponential backoff; anything else is handed to you to decide about.

No, and that is intentional: automatic encryption would mean the SDK holding your passphrase for the lifetime of the client. It exposes encryptValue and decryptValue, encrypt_value and decrypt_value in Python and Ruby, and Envless::encryptValue and Envless::decryptValue in PHP, so encryption is an explicit step you control, and the API refuses any value that is not already an envelope, so a forgotten call fails loudly with variable.ciphertext_invalid rather than silently storing plaintext.

Use variables.apply with a baseline. You send the id, name and updatedAt of every variable you based your change on; the server takes an advisory lock on the environment, recomputes drift, and rejects the whole call with 409 variable.baseline_drifted naming what changed. That is compare-and-set for a whole environment, and it is what the dashboard raw editor uses.

TypeScript, Python, Ruby and PHP ship today: @goenvless/sdk on npm, envless-sdk on PyPI with the same 113 methods, a synchronous client and an asynchronous one, envless-sdk on RubyGems with the same methods and RBS types, and envless/sdk on Packagist with the same methods and PHPStan and Psalm types. Underneath is a plain REST API: bearer token, JSON, standard status codes, and an OpenAPI document you can generate a client from in any other language. The encryption is the part worth porting carefully, and the encryption reference page documents the exact parameters so an implementation in another language interoperates.

A key carries a map over eleven resources at three levels: none, read or write. Each route declares what it needs and the check is a rank comparison, so a miss returns 403 auth.scope_missing naming the exact scope required. GET /me is the only public route with no scope requirement. Workspace RBAC is a separate, finer catalogue of 45 named permissions that governs people rather than keys.