HyphaBase

Server apps

Sarah's Sunday email job, and the cleaner's phone app. Two credentials, and the difference is whose authority the request carries.

Two things run outside a browser in Sarah's setup, and they sign in differently.

The Sunday email job runs at 6am and sends everyone their week's visits. Nobody's signed in; it acts as Brightside Cleaning itself. That's a secret key.

The cleaners' phone app shows Marcus his own rounds. It acts as Marcus, and shouldn't show him Salem's work. That's a personal token.

Acts asReach for it when
Secret key sk_…Your softwareBackground jobs, syncs, integrations, webhook handlers
Personal tokenOne personA mobile app or CLI where the audit trail should name the human

Undecided? You want a secret key. Personal tokens exist for clients that hold someone's password but can't use cookies.

Secret keys

Two environment variables and you're going:

HYPHABASE_SECRET_KEY=sk_live_xxx
HYPHABASE_LOCATION_ID=019aff48-a982-70b4-9be8-57eb265122d7

Write the wrapper once, so headers, idempotency and error handling aren't re-derived at every call site:

const BASE = 'https://api.hyphabase.com';

export async function api(path, { idempotencyKey, ...init } = {}) {
  const response = await fetch(`${BASE}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.HYPHABASE_SECRET_KEY}`,
      'X-Location-Id': process.env.HYPHABASE_LOCATION_ID,
      'Content-Type': 'application/json',
      Accept: 'application/json',
      ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
      ...init.headers,
    },
  });

  if (!response.ok) {
    const body = await response.json().catch(() => ({}));
    throw Object.assign(new Error(body.message ?? response.statusText), {
      status: response.status,
      errors: body.errors,
    });
  }

  return response.status === 204 ? null : response.json();
}

In PHP:

$response = Http::withToken(config('hyphabase.secret_key'))
    ->withHeaders(['X-Location-Id' => config('hyphabase.location_id')])
    ->acceptJson()
    ->get('https://api.hyphabase.com/api/modules/clients');

Serving several sites from one job

The site id is per request, not per key. Sarah's Sunday job uses one key and loops:

export const forSite = (siteId) => (path, init = {}) =>
  api(path, { ...init, headers: { 'X-Location-Id': siteId, ...init.headers } });

for (const site of await ourSites()) {
  const send = forSite(site.id);
  const { data: customers } = await send('/api/modules/clients');
  await emailCustomerReport(site, customers);
}

A key that hasn't been granted a site returns 403 — see when a request is refused. Granting the whole group rather than each site means new sites are picked up automatically.

Tokens for a specific person

Swap someone's email and password for a token:

curl -X POST https://api.hyphabase.com/api/auth/login \
  -H "Content-Type: application/json" \
  -H "X-Location-Id: $HYPHABASE_LOCATION_ID" \
  -d '{"email":"marcus@brightsideclean.example","password":"…","device_name":"Marcus iPhone"}'
FieldNotes
emailUnique within a site, not globally — the same address can exist at two of your sites.
password
device_nameOptional but worth sending. Labels the token so one phone can be revoked without signing out the others.

You get back token, the person, and their permissions. Then:

Authorization: Bearer 12|kGz9...
X-Location-Id: 019aff48-a982-70b4-9be8-57eb265122d7
ForEndpoint
Get a tokenPOST /api/auth/login
Who is thisGET /api/auth/me
Revoke this onePOST /api/auth/logout

Tokens last 120 minutes and there's no refresh token

Treat a 401 as "ask them to sign in again", and store the token somewhere it disappearing is survivable. Signing out deletes only the token that made the call, so Marcus's tablet stays signed in when he signs out on his phone.

Sign-in is limited to 5 attempts a minute per email and address, and 20 a minute per address. Never retry a failed sign-in in a loop — you'll lock out the real person.

Handling webhooks

The natural pairing: something happens, we tell your server, you use your secret key to do whatever comes next.

app.post('/webhooks/hyphabase', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verify(process.env.WEBHOOK_SECRET, req.body, req.header('HyphaBase-Signature'))) {
    return res.sendStatus(400);
  }

  res.sendStatus(200);          // acknowledge fast
  enqueue(JSON.parse(req.body.toString()));   // then do the work
});

Full treatment in Getting told when things happen.

Next

On this page