@wtfalch/providers
v0.3.0
Published
Typed clients for Coolify, Cloudflare, GitHub and ZITADEL. Every client takes its credential as an argument; none of them reads env or a secrets file.
Readme
@wtfalch/providers
Typed clients for the systems the estate runs on: Coolify, Cloudflare, GitHub and ZITADEL. Every client takes its credential as a constructor argument — none of them reads an environment variable or a secrets file.
Status
Published: yes, v0.1.0, 2026-09-22.
See the repo README for why this package exists and what still needs deciding.
Usage
import { CoolifyClient } from '@wtfalch/providers/coolify';
const coolify = new CoolifyClient('https://coolify.example.com', token);
const projects = await coolify.listProjects();import { CloudflareClient } from '@wtfalch/providers/cloudflare';
const cloudflare = new CloudflareClient(token, accountId);
const zones = await cloudflare.listZones();import { GitHubClient } from '@wtfalch/providers/github';
const github = new GitHubClient();
const exists = await github.repoExists('wtfalch/providers');import { ZitadelClient } from '@wtfalch/providers/zitadel';
const zitadel = new ZitadelClient('https://auth.wtfalch.dev', token);
const { org, created } = await zitadel.findOrCreateOrg('acme');
const { project } = await zitadel.findOrCreateProject(org.id, 'app-one');
const { appId, clientId } = await zitadel.createOidcApp(org.id, project.id, {
name: 'web',
appType: 'web',
redirectUris: ['https://app-one.example.com/auth/callback'],
postLogoutRedirectUris: ['https://app-one.example.com/'],
});listResources
Coolify, Cloudflare and ZITADEL each expose listResources(scope), returning
LiveResource[] (@wtfalch/providers/resource) — everything this package
can see in that scope, for a plan to diff against. scope is that provider's
own natural key (a Coolify project uuid, a Cloudflare zone id, a ZITADEL org
id), not an estate-wide org id: resolving an estate org to a provider's scope
is foundry's job, not this package's (see ADR 0002).
- Coolify (
projectUuid):environment(keyed by uuid),applicationanddatabase(both keyed by uuid, scoped to the project's environments). - Cloudflare (
zoneId):zone(keyed by the zone's name),dns_record(keyed by record id),email_routing(keyed by the zone id), andtunnel(keyed by tunnel id) — every non-deleted tunnel in the zone's owning account, not filtered to ones that route a hostname in this zone. - ZITADEL (
orgId):project(keyed by id),project_grant(keyed by grant id), andapp(keyed by the app's own ZITADEL id, since a name is only unique within one project) — oneappper OIDC or API application in each project,configcarryingname,projectIdandappType(one of'web','native'or'api').
GitHub has no listResources: nothing in the estate declares a reader for
it yet.
ZITADEL app creation
ZitadelClient can create the resources foundry's step executor needs, all
find-or-create except apps (see below): findOrCreateOrg,
findOrCreateProject, createOidcApp and createApiApp.
createOidcApp(orgId, projectId, { name, appType, redirectUris, postLogoutRedirectUris })—appTypeis'web'or'native'. Both use the authorization-code grant with PKCE and no client secret (OIDC_AUTH_METHOD_TYPE_NONE);nativehere is a redirect-based public client (a loopback or custom-scheme URI), not the device-code flowauth/scripts/provision.mjs's ownnativeapp kind uses. Returns{ appId, clientId }— no secret, since none is ever issued.createApiApp(orgId, projectId, { name })—API_AUTH_METHOD_TYPE_BASIC, matchingprovision.mjs. Returns{ appId, clientId, clientSecret }: ZITADEL hands back the secret once, on creation, and never shows it again, so the caller must store it immediately. Never put a secret in a log line or an error.deleteApp(orgId, projectId, appId)— an app's OIDC/API config cannot be converted in place, so a kind change deletes and recreates it under the same name. There is nofindOrCreateApp: apps are looked up throughlistAppsand recreated on a mismatch, not matched by name alone.
None of this is verified against a live ZITADEL instance (see ADR 0004).
