{"id":"69ecef9a60bb8b34fce600a1","title":"Tenants And Tokens","path":"how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock/tenants-and-tokens","contentMarkdown":"# 21-Apr-2026 — Tenants and API tokens\n\n_____\n\nA **tenant** is the scope your data lives in. An **API token** authorises calls against exactly one tenant. You will need at least one of each before the public API will return anything.\n\n[← Back to the overview](https://ngx-ramblers.org.uk/how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock)\n\n## Tenants\n\nA tenant is either:\n\n- A **group** with a 4-character `groupCode` (e.g. `KT50` for East Kent Walking Group), or\n- An **area** with a 2-character `areaCode` (e.g. `KT` for Kent Area)\n\nThe kind matters for the API path: groups are queried as `/api/groups/{code}/members`, areas as `/api/areas/{code}/members`.\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/915719e3-2d02-4314-b24a-e32ca99ffef7.png)\n\n*The dashboard sidebar lists every tenant you own. Click + New to add one, or click an existing code (KT, KT06) to open its detail pane.*\n\n### Create a tenant\n\nIn the dashboard sidebar, click **+ New** next to \"Tenants\" and fill in:\n\n- **Code** — 2 to 6 alphanumeric characters (`KT50`, `KT`, `NS03`)\n- **Kind** — `group` or `area`\n- **Display name** — human-readable, optional (e.g. `East Kent Walking Group`)\n\nHit **Create**. The tenant appears in the sidebar; click it to open the detail pane, which has three tabs: **Members**, **Generate**, and **Tokens**.\n\n### Tenant codes are globally unique\n\nTenant codes are unique across the whole mock server, not just per operator. If `KT50` has already been claimed by someone else, you'll get a 409 conflict on create. Pick a different code, or [request a transfer](https://ngx-ramblers.org.uk/?contact-us&role=support&subject=Salesforce%20Mock%20-%20Tenant%20transfer%20request&redirect=how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock/tenants-and-tokens).\n\nThis is intentional: the public API path `/api/groups/{code}/members` would otherwise be ambiguous, and downstream consumers wouldn't know whose dataset they were calling against.\n\n## API tokens\n\nOpen the tenant's **Tokens** tab. Each token authorises calls against this tenant only — there is no concept of a multi-tenant or operator-wide token.\n\n![](https://ngx-ramblers.org.uk/api/aws/s3/site-content/c10cf889-2281-4ee1-9c62-7d288b6103e5.png)\n\n*The Tokens tab inside a tenant's detail pane. Type a label, hit Generate token, and copy the revealed plaintext immediately — it's only shown once.*\n\n### Generate a token\n\nType a human-readable label (the input placeholder shows `MailMan - NS03 dev` as one example; `NGX production sync` and `James local laptop` are equally fine — the label is just for your own bookkeeping) and hit **Generate token**.\n\nA panel reveals the full token string with a **Copy** button. **Copy it now.** The plaintext is shown exactly once; on the next page load all you'll see is the 16-character prefix in the table below. The server stores only a SHA-256 hash, so nobody — not even the root operator — can retrieve the original after generation.\n\n### Token format\n\n```\nrsm_<tenantCode>_<48-hex-random>\n```\n\nThe tenant code is baked into the plaintext. That makes the prefix self-identifying: skim a log, see `rsm_KT50_…`, and you immediately know which tenant the request was scoped to. The 48-hex suffix is generated from `crypto.randomBytes(24)`, so the keyspace is 192 bits.\n\n### Where to store it\n\nA password manager, a [GitHub Actions secret](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions), a Fly.io secret, or whatever your team uses — anywhere except a checked-in file.\n\n### Revoke a token\n\nIn the same Tokens table, every non-revoked row has a **Revoke** button. Revocation is immediate and one-way: the token row stays in the table for audit, but any call presenting that token will fail with 401.\n\nIf you lose a token, revoke it and generate a new one. There is no recovery path.\n\n## Security model summary\n\n- Tokens are **shown in plaintext only once**, when generated. Server stores a SHA-256 hash.\n- Tokens are **scoped to exactly one tenant**. A token for `KT50` cannot read `NS03`; the API returns 401.\n- **Per-operator isolation** — every query is filtered by `tenantCode` (data) or `ownerOperator` (admin). Operators cannot see each other's tenants, tokens, members, or consent events.\n- **Pre-production only.** Do not put real production member data in here. Synthetic data for demos; real Insight Hub exports only for focused integration testing into a clean tenant.\n\n## Next\n\n[**Loading member data →**](https://ngx-ramblers.org.uk/how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock/loading-member-data)","contentHtml":"<h1>21-Apr-2026 — Tenants and API tokens</h1>\n<hr>\n<p>A <strong>tenant</strong> is the scope your data lives in. An <strong>API token</strong> authorises calls against exactly one tenant. You will need at least one of each before the public API will return anything.</p>\n<p><a href=\"https://ngx-ramblers.org.uk/how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock\">← Back to the overview</a></p>\n<h2>Tenants</h2>\n<p>A tenant is either:</p>\n<ul>\n<li>A <strong>group</strong> with a 4-character <code>groupCode</code> (e.g. <code>KT50</code> for East Kent Walking Group), or</li>\n<li>An <strong>area</strong> with a 2-character <code>areaCode</code> (e.g. <code>KT</code> for Kent Area)</li>\n</ul>\n<p>The kind matters for the API path: groups are queried as <code>/api/groups/{code}/members</code>, areas as <code>/api/areas/{code}/members</code>.</p>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/915719e3-2d02-4314-b24a-e32ca99ffef7.png\" alt=\"\"></p>\n<p><em>The dashboard sidebar lists every tenant you own. Click + New to add one, or click an existing code (KT, KT06) to open its detail pane.</em></p>\n<h3>Create a tenant</h3>\n<p>In the dashboard sidebar, click <strong>+ New</strong> next to &quot;Tenants&quot; and fill in:</p>\n<ul>\n<li><strong>Code</strong> — 2 to 6 alphanumeric characters (<code>KT50</code>, <code>KT</code>, <code>NS03</code>)</li>\n<li><strong>Kind</strong> — <code>group</code> or <code>area</code></li>\n<li><strong>Display name</strong> — human-readable, optional (e.g. <code>East Kent Walking Group</code>)</li>\n</ul>\n<p>Hit <strong>Create</strong>. The tenant appears in the sidebar; click it to open the detail pane, which has three tabs: <strong>Members</strong>, <strong>Generate</strong>, and <strong>Tokens</strong>.</p>\n<h3>Tenant codes are globally unique</h3>\n<p>Tenant codes are unique across the whole mock server, not just per operator. If <code>KT50</code> has already been claimed by someone else, you&#39;ll get a 409 conflict on create. Pick a different code, or <a href=\"https://ngx-ramblers.org.uk/?contact-us&role=support&subject=Salesforce%20Mock%20-%20Tenant%20transfer%20request&redirect=how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock/tenants-and-tokens\">request a transfer</a>.</p>\n<p>This is intentional: the public API path <code>/api/groups/{code}/members</code> would otherwise be ambiguous, and downstream consumers wouldn&#39;t know whose dataset they were calling against.</p>\n<h2>API tokens</h2>\n<p>Open the tenant&#39;s <strong>Tokens</strong> tab. Each token authorises calls against this tenant only — there is no concept of a multi-tenant or operator-wide token.</p>\n<p><img src=\"https://ngx-ramblers.org.uk/api/aws/s3/site-content/c10cf889-2281-4ee1-9c62-7d288b6103e5.png\" alt=\"\"></p>\n<p><em>The Tokens tab inside a tenant&#39;s detail pane. Type a label, hit Generate token, and copy the revealed plaintext immediately — it&#39;s only shown once.</em></p>\n<h3>Generate a token</h3>\n<p>Type a human-readable label (the input placeholder shows <code>MailMan - NS03 dev</code> as one example; <code>NGX production sync</code> and <code>James local laptop</code> are equally fine — the label is just for your own bookkeeping) and hit <strong>Generate token</strong>.</p>\n<p>A panel reveals the full token string with a <strong>Copy</strong> button. <strong>Copy it now.</strong> The plaintext is shown exactly once; on the next page load all you&#39;ll see is the 16-character prefix in the table below. The server stores only a SHA-256 hash, so nobody — not even the root operator — can retrieve the original after generation.</p>\n<h3>Token format</h3>\n<pre><code>rsm_&lt;tenantCode&gt;_&lt;48-hex-random&gt;\n</code></pre>\n<p>The tenant code is baked into the plaintext. That makes the prefix self-identifying: skim a log, see <code>rsm_KT50_…</code>, and you immediately know which tenant the request was scoped to. The 48-hex suffix is generated from <code>crypto.randomBytes(24)</code>, so the keyspace is 192 bits.</p>\n<h3>Where to store it</h3>\n<p>A password manager, a <a href=\"https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions\">GitHub Actions secret</a>, a Fly.io secret, or whatever your team uses — anywhere except a checked-in file.</p>\n<h3>Revoke a token</h3>\n<p>In the same Tokens table, every non-revoked row has a <strong>Revoke</strong> button. Revocation is immediate and one-way: the token row stays in the table for audit, but any call presenting that token will fail with 401.</p>\n<p>If you lose a token, revoke it and generate a new one. There is no recovery path.</p>\n<h2>Security model summary</h2>\n<ul>\n<li>Tokens are <strong>shown in plaintext only once</strong>, when generated. Server stores a SHA-256 hash.</li>\n<li>Tokens are <strong>scoped to exactly one tenant</strong>. A token for <code>KT50</code> cannot read <code>NS03</code>; the API returns 401.</li>\n<li><strong>Per-operator isolation</strong> — every query is filtered by <code>tenantCode</code> (data) or <code>ownerOperator</code> (admin). Operators cannot see each other&#39;s tenants, tokens, members, or consent events.</li>\n<li><strong>Pre-production only.</strong> Do not put real production member data in here. Synthetic data for demos; real Insight Hub exports only for focused integration testing into a clean tenant.</li>\n</ul>\n<h2>Next</h2>\n<p><a href=\"https://ngx-ramblers.org.uk/how-to/technical-articles/2026-04-21-using-ramblers-salesforce-mock/loading-member-data\"><strong>Loading member data →</strong></a></p>\n"}