Skip to content

Get started with ZedGi ​

Connect any TCP database to the edge in minutes. No open ports. No VPNs. Just HTTPS.

Trusted by teams at Vercel, Linear, and 140+ others

1. Create an account and service ​

Sign in at zedgi.app using GitHub or Google. Once inside the dashboard, click New Service.

Choose your database type (Redis, Postgres, or MySQL), paste your connection string, and optionally restrict access to specific IPs.

2. Generate an API key ​

After the service is created, go to the service detail page and click Create API Key.

Zedgi uses a two-value key model:

ValueHeaderPurpose
keyx-zedgi-keyPublic key identifier (starts zk_)
signing_secret(used to sign)HMAC-SHA256 secret for request auth

Both values are shown only once at creation — store them securely. The key is hashed with SHA-256 before storage and cannot be recovered. The signing_secret is encrypted with AES-256-GCM on the server.

bash
# Example: what the API key endpoint returns
{
  "id": "a1b2c3d4e5f6...",
  "key": "zk_live_1a2b3c4d5e6f7890abcdef1234567890abcdef12",
  "signing_secret": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
  "label": "production",
  "message": "Save both values — they will not be shown again."
}
â„šī¸Ž
Keys are encrypted at rest with AES-256-GCM. They are only decrypted inside the Cloudflare Worker runtime.

3. Install the client ​

Terminal
bash
npm install @zedgi/zedgi-client

4. Authenticating requests ​

Every Zedgi RPC request requires the API key identifier plus signing headers. When you also supply a credential, the x-zedgi-cred "link" (encrypted credentials) is added automatically by the client. The x-zedgi-key identifies you; the ts/nonce/sig trio prove you hold the signing secret and that the request is fresh.

HeaderDescription
x-zedgi-keyYour API key identifier (the key from step 2)
x-zedgi-tsCurrent epoch time in milliseconds
x-zedgi-nonce32-char random lowercase hex (128-bit, single-use per key)
x-zedgi-sigHMAC-SHA256 of {ts}:{nonce}:{hex(sha256(body))}

Computing the signature ​

message   = `${ts}:${nonce}:${sha256hex(jsonBody)}`
x-zedgi-sig = HMAC-SHA256(message, signing_secret)

The body is the raw JSON string of the RPC payload — not a JSON object, not pretty-printed. Use JSON.stringify(body) to normalize it.

Step-by-step signing ​

ts
import { createHash, createHmac, randomBytes } from 'node:crypto';

function signRequest(
  body: Record<string, unknown>,
  apiKey: string,
  signingSecret: string
): Headers {
  const ts = Date.now().toString();
  const nonce = randomBytes(16).toString('hex'); // 32 hex chars
  const bodyJson = JSON.stringify(body);
  const bodySha256 = createHash('sha256').update(bodyJson).digest('hex');
  const message = `${ts}:${nonce}:${bodySha256}`;
  const sig = createHmac('sha256', signingSecret)
    .update(message)
    .digest('hex');

  return new Headers({
    'content-type': 'application/json',
    'x-zedgi-key': apiKey,
    'x-zedgi-ts': ts,
    'x-zedgi-nonce': nonce,
    'x-zedgi-sig': sig,
  });
}
python
import hashlib
import hmac
import json
import secrets
import time

def sign_request(body: dict, api_key: str, signing_secret: str) -> dict[str, str]:
    ts = str(int(time.time() * 1000))
    nonce = secrets.token_hex(16)
    # Compact JSON, same as JSON.stringify
    body_json = json.dumps(body, separators=(",", ":"))
    body_sha256 = hashlib.sha256(body_json.encode("utf-8")).hexdigest()
    message = f"{ts}:{nonce}:{body_sha256}"
    sig = hmac.new(
        signing_secret.encode("utf-8"),
        message.encode("utf-8"),
        hashlib.sha256,
    ).hexdigest()

    return {
        "content-type": "application/json",
        "x-zedgi-key": api_key,
        "x-zedgi-ts": ts,
        "x-zedgi-nonce": nonce,
        "x-zedgi-sig": sig,
    }
http
POST /rpc HTTP/1.1
Host: acme.zedgi.app
content-type: application/json
x-zedgi-key: zk_live_1a2b3c4d5e6f7890abcdef1234567890abcdef12
x-zedgi-ts: 1749600123456
x-zedgi-nonce: a1b2c3d4e5f67890abcdef1234567890
x-zedgi-sig: 8f7e6d5c4b3a291807f6e5d4c3b2a190f8e7d6c5b4a39281706f5e4d3c2b1a09
x-zedgi-cred: AQ... (ECIES-encrypted credentials blob — see next section)

{"service":"redis","method":"get","payload":{"args":["mykey"]}}

The official clients compute and attach these headers (plus x-zedgi-cred when you pass a credential) automatically — the signing secret is auto-pulled and cached, so you don't supply it. (signingSecret is still accepted if you'd rather manage it yourself; secret is a deprecated alias.)

signingSecret vs publicKey — two keys, two jobs ​

These are easy to confuse, so here's the difference:

signingSecret — proves the request is really from you and wasn't tampered with. A shared secret (symmetric, HMAC-SHA256). The client signs each request — HMAC(ts:nonce:sha256(body), signingSecret) → the x-zedgi-sig header. The gateway holds the same secret, recomputes it, and checks they match. That proves the caller holds the secret (authenticity), the body wasn't altered in transit (integrity), and — with the timestamp + single-use nonce — that it isn't a replay. It protects the request itself.

publicKey — encrypts your DB credentials so only ZedGi's gateway can read them. The public half of an X25519 keypair (asymmetric). The client encrypts your credential to this public key (ECIES). The matching private key is held by ZedGi (wrapped with the Master KEK) and is used inside the gateway to decrypt. The gateway then re-encrypts the credential to the proxy node's key for the final hop; the proxy decrypts it transiently to open the TCP connection. So plaintext exists only momentarily inside the gateway and the proxy — never on the network, never at rest. It protects the credential content (confidentiality).

signingSecretpublicKey
TypeSymmetric (same secret both sides)Asymmetric (public encrypts, private decrypts)
JobAuthenticate the request; detect tampering/replayHide your DB credentials in transit
ProtectsThe request envelopeThe credential payload
OperationSigning (HMAC)Encryption (ECIES)
Decrypted/verified byThe gateway (HMAC compare)The gateway (with your account private key)

Analogy: the signing secret is a tamper-proof signature on the envelope; the public key is a lockbox only the recipient (the gateway) can open.

ZedGi never stores your database credentials. Services only record target endpoint metadata: a host and, when you provide one, a port.

Link logic:

  • At call time you (or the SDK) supply your real credentials.
  • The client encrypts them client-side with your per-account X25519 public key (ECIES).
  • If your credential object includes header, that header object is excluded from ECIES encryption and sent as signed plaintext metadata for proxy/firewall integrations.
  • The ciphertext is sent as the x-zedgi-cred header — this is the "link" that travels with every request.
  • The gateway re-encrypts the blob (never materializes plaintext) for the specific proxy node.
  • Only the proxy transiently decrypts to open the TCP connection to your real Redis/Postgres/MySQL.

The encrypted blob is account-specific and versioned (includes key_version). This enables safe key rotation.

Get your current public key (for encryption) ​

Auto-pull (recommended): omit publicKey when creating the client and supplying a credential. The SDK will call:

GET /api/account/keys/current
x-zedgi-key: zk_...

and cache the response.

Manual / pinned (faster startup, explicit):

http
GET /api/account/keys/current
x-zedgi-key: zk_live_...

Response:

json
{
  "id": "9f2c...32-hex...",
  "key_version": 1,
  "public_key": "lRk7...base64url...",
  "created_at": 1749600000000
}

Copy the public_key into your environment as ZEDGI_PUBKEY (or pass it directly). When pinning manually, also pass accountId (the id above) and keyVersion so the SDK skips the auto-pull entirely.

6. Use the client (with credential linking) ​

TypeScript
ts
import { createZedgiClient } from '@zedgi/zedgi-client';

// Full / explicit form — every field annotated. Only `url`, `key`, and `credential`
// are needed in practice; everything else is auto-fetched (see comments).
const zedgi = createZedgiClient({
  url: 'https://acme.zedgi.app',               // your personal Zedgi endpoint (the gateway) you got at sign-up — NOT your database
  key: process.env.ZEDGI_KEY!,                 // REQUIRED — your API key identifier (zk_...), from dashboard → service → New key. Sent as x-zedgi-key
  signingSecret: process.env.ZEDGI_SECRET!,    // OPTIONAL — HMAC secret used to sign each request. Auto-pulled + cached when omitted; pin it only to skip the fetch
  publicKey: process.env.ZEDGI_PUBKEY!,        // OPTIONAL — your account X25519 public key; your credential is encrypted to it (the GATEWAY decrypts). Auto-pulled when omitted
  accountId: process.env.ZEDGI_ACCOUNT_ID!,    // OPTIONAL — the `id` from /api/account/keys/current. Auto-pulled with publicKey
  keyVersion: 1,                               // OPTIONAL — the `key_version` from the same endpoint. Auto-pulled with publicKey
  credential: {                                // YOUR database's own login (encrypted client-side; Zedgi never sees plaintext)
    host: 'my-db.example.com',                 // OPTIONAL — normally unnecessary: the gateway uses the host you set when registering the service
    port: 5432,                                // OPTIONAL — same as host; defaults to the adapter's standard port
    user: 'app',                               // your DB username
    password: process.env.DB_PASSWORD!,        // your DB password
    database: 'myapp',                         // the database/schema to use (Postgres/MySQL)
    header: {                                  // OPTIONAL — signed-but-NOT-encrypted metadata forwarded to firewalls/proxies
      'x-firewall-token': process.env.DB_FIREWALL_TOKEN!,
    },
  },
  cache: true,                                 // OPTIONAL — encrypt the credential once and reuse the ciphertext (default true)
});

// In practice this is all you write — the rest is automatic:
const zedgiAuto = createZedgiClient({
  url: 'https://acme.zedgi.app',               // your Zedgi endpoint
  key: process.env.ZEDGI_KEY!,                 // your zk_... key
  credential: {                                // your DB secrets (host/port come from the service)
    user: 'app',
    password: process.env.DB_PASSWORD!,
    database: 'myapp',
  },
});

// Use exactly like the native driver
const redis = zedgi.redis();
await redis.set('user:42', JSON.stringify({ name: 'Ada' }));

const pg = zedgi.postgres();
const { rows } = await pg.query('SELECT NOW()');

From just url + key + credential, the client automatically:

  • Pulls + caches your signing secret and account public key (using your key).
  • Encrypts credential → x-zedgi-cred (the link), then signs every request (x-zedgi-ts + x-zedgi-nonce + x-zedgi-sig).

Minimal (headers managed by you) ​

If you only need the RPC facade and will attach headers yourself:

ts
const zedgi = createZedgiClient({
  url: 'https://acme.zedgi.app',
  key: process.env.ZEDGI_KEY!,
});

Raw HTTP (any language / custom signing) ​

See the Authentication & Security guide for the exact header computation and blob format.

7. Key rotation ​

Rotate your account encryption keypair (the X25519 key your credentials are encrypted to) at any time.

From the dashboard: open Overview and click Rotate key. You'll see the new key version immediately.

From the API:

http
POST /api/account/keys/rotate
Authorization: Bearer <session>

What happens:

  • A new active keypair is created; the previous one is marked retired (it keeps working so in-flight credential blobs don't break).
  • You receive a notification email.

How the SDK handles it — usually nothing to do:

  • A new client instance auto-pulls the active key on first use, so it encrypts to the new key automatically.
  • A long-lived client in auto mode (you didn't pin publicKey) self-heals: if a call ever fails because its cached key is outdated, the SDK drops the cache, re-pulls the current public key, re-encrypts, and retries once.
  • If you pinned publicKey/accountId/keyVersion yourself, the auto-recovery is off — fetch the new values from GET /api/account/keys/current and update your config.

API keys (zk_* identifier + signing secret) are completely unaffected by rotation — only the credential-encryption key changes.

See the full details in the Authentication & Security guide.


Free tier & pricing ​

Every account receives 100 requests per day for free. No credit card required to start.

When you need more, deposit any amount from the Billing page (minimum $5). Usage is billed at $10 per million requests and deducted automatically each night.

See the full Redis, Postgres, and MySQL command references, or learn about custom hooks.


Security model

  • API keys use a two-value model: a public identifier (zk_...) and an HMAC signing secret (shown once).
  • The "link" (x-zedgi-cred) carries credentials encrypted client-side with your account X25519 public key (ECIES). ZedGi never stores plaintext credentials.
  • Requests are protected by timestamp + single-use nonce + HMAC-SHA256 signature.
  • The credential blob is re-encrypted at the gateway for the RPC node (double hop). Plaintext exists only transiently inside the proxy when opening the TCP connection to your database.
  • Account key rotation is supported with auto public-key discovery in the SDK and a grace period for old blobs.
  • All traffic over HTTPS. Optional IP whitelisting per service.