Before you start#

You needWhy
A HubSpot portal with custom objectsKeyring's entire data model is custom CRM objects. Without that entitlement there is nothing to install into
Super admin on that portalThe install grants scopes, and several steps happen in Design Manager and the workflow editor
A published website domain on the portalThe customer wallet and the staff scanner are pages on the portal's own site. The wallet URL is derived from the primary site domain, not configured
Marketing email, if you want the loyalty card deliveredThe loyalty-card email uses HubSpot's programmable email, which has to be switched on per module
Workflows, if you want automated enrolment, expiry or tiersThe workflow actions are optional but are how most of the lifecycle runs

Important

Before a real-data deployment, ask for the hardening pass. Keyring today carries deliberate demo-grade trade-offs — most consequentially, the installer endpoint is deliberately open so the app can bootstrap itself on a fresh portal, and there is no per-till revocation. Both are closable, and there is a defined checklist for doing it, but neither closes by itself. This is a real prerequisite, and we would rather say so here than discover we agreed otherwise later.

Who does what#

StepWho
Deploying the app to the portal and granting scopesUs, with a portal admin present
Running the one-call bootstrap that creates the schema, settings and standard pagesUs
Deciding the venues, their capabilities, and the programmesYou — see below. These are business decisions with security consequences
Creating staff identities and setting the PINYou
Enabling the email module and switching the lifecycle workflows onEither, but a portal admin must approve — they mutate live records
Verifying the chain end to endBoth, together. See Verifying it works

What the install provisions#

The bootstrap is one idempotent call. Running it twice changes nothing, and it returns no secrets. It creates:

  • The custom objects — venues, programmes, memberships, the transaction ledger — and extends the incentive object with the value and journey property layers. On a portal that has never had one, the incentive object is created outright.
  • Three contact properties — the magic link, the QR image, and a locale hint.
  • The association labels. These matter: an incentive relates to a venue in two different ways, so the labels are the contract, not decoration.
  • The settings records, seeded with defaults. Secrets are seeded empty, never with a value from code.
  • The standard pages for the wallet, the scanner and enrolment, when asked to.
  • Two lifecycle workflows — created switched off, when asked to. They are voucher expiry and tier promotion, and they are disabled deliberately so that an install never silently mutates existing records.

The decisions only you can make#

Venues and capabilities#

This is the most consequential configuration in the app, and it is the one most often waved through.

Every venue record carries capabilities: whether staff signed in there may identify, redeem, earn or issue. Every mutation re-checks that list on the server, against the venue record — so the toggles in the staff app are cosmetic and a compromised or misconfigured device cannot exceed what its venue permits.

Give each venue the narrowest set that lets it work. A bar that only accepts vouchers gets redeem and nothing else. The one to think hardest about is issue: it is the capability that lets a counter create value out of nothing, and it belongs at a small number of supervised venues.

An online checkout integrating with Keyring is a venue too — give it redeem only.

Staff identities and the PIN#

Staff sign in as contacts on the portal. A shared till is a contact of its own — a machine identity, named for what it is. That is what gives the ledger per-till attribution.

Set the PIN. There is a setting that additionally refuses staff sign-in while no PIN is configured; on any install that matters, turn it on so the safe state is the default rather than the diligent one.

Programmes#

A programme defines what a point is called, what it is worth per unit spent, and whether it applies across the brand or at named venues. Get the earn rate right before anyone earns anything — changing it later does not retro-fit balances, and it should not.

The manual steps#

These genuinely cannot be automated, in this order:

  1. Install the app and grant the scopes. Confirm afterwards that the portal reports the app installed with no outstanding scope changes. Until it does, anything needing a newly added scope fails in ways that look like unrelated bugs — automation, page provisioning and the wallet URL each depend on one.
  2. Enable the loyalty email module for programmable email. In Design Manager, open the Keyring loyalty-card module, tick the programmable-email option in the inspector, and publish. The balances block resolves only when this is on — and only in the email send preview for a specific contact, never in the Design Manager editor preview. That difference has cost more time than the step itself.
  3. Wire the magic-link workflow action. Once the app is installed with automation, the action appears in the workflow picker. The usual shape is: enrol → mint the member link → send the loyalty email. The action writes the link and QR onto the contact; the email reads them from there.
  4. Review and switch on the lifecycle workflows — voucher expiry and tier promotion. Read what each one enrols before enabling it.
  5. Decide the QR image provider — a data-flow decision, not just a rendering one; see Security posture before accepting the default. The install points it at our hosted generator. If you change it to any other host, that host must also be added to the app's image allow-list and the app re-uploaded, or the QR renders as a blocked image inside HubSpot's extension sandbox — the wallet and scanner pages are ordinary web pages and are unaffected, which is why this fails in only one of the two places.
  6. Paste the label-printer token, if physical loyalty cards are being printed. It is seeded empty and never lives in code. The settings page has a test print.
  7. Lock the installer, if this portal holds anything real. Once bootstrap has run, adding the install-key secret makes the installer endpoint require it. The gate enforces itself as soon as the secret exists.

What the app can reach#

The scopes it requests, in full, because a reviewer should not have to take "the usual ones" on trust:

ScopeWhy
crm.objects.contacts.read · .writeMembers and staff are contacts. Write covers enrolment, and the three added properties for the loyalty card
crm.objects.custom.read · .writeVenues, programmes, memberships, the ledger and incentives are all custom objects
crm.schemas.custom.read · .writeOnly used at install, to create those objects and their properties. Without it the app cannot provision itself
crm.objects.products.readProduct vouchers resolve against your product catalogue. Read only
automationThe workflow actions — mint a card, enrol, issue a voucher, award points. Drop this and the lifecycle automation is unavailable; nothing else breaks
contentProvisioning the wallet, scanner and enrolment pages at install
cms.domains.readReading the portal's primary domain, which is how the wallet URL is derived rather than configured
e-commerceProduct and pricing reads behind product vouchers
oauthThe install itself

Two things a reviewer usually asks next. There is no crm.objects.deals, no tickets, no email or marketing scope — Keyring has no reason to read your pipeline or your campaigns, and does not. And adding a scope later requires a reinstall; until that happens the affected functions fail rather than degrade, which is why the install step insists on confirming there are no outstanding scope changes.

Security posture#

The shareable summary, for a reviewer who has to sign this off:

  • Customer data stays on your portal. Keyring's records are CRM records in your own HubSpot account. We do not operate a database holding your customers.
  • One thing does leave the portal, and you should decide about it deliberately: QR image rendering. A loyalty-card QR is an <img> in an email, and HubSpot's serverless functions return JSON only — they cannot serve an image — so the image URL points at an external renderer. The URL handed to that renderer contains the member's wallet link, and therefore their token. The install points this at our own hosted renderer; the built-in fallback is a public third-party QR service. Neither stores anything, but both see the link. If that is not acceptable to your review, point the setting at a renderer you host — it is a URL template, and swapping it needs no redeploy on our side. This is the only outbound data flow in the app, and it is worth being explicit about rather than letting it be found.
  • Tokens are stateless and signed, from a secret held in your portal's settings. Verification is a signature check with no database lookup; the signature is verified before the payload is parsed, so a tampered token never reaches a contact lookup.
  • Identity responses are data-minimised. Scanning a member card returns a name — never an email address or a phone number. That is enforced server-side, not a UI choice.
  • Authority is per venue and re-checked server-side on every mutation.
  • The ledger is append-only. Reversals write a compensating row; nothing is edited away.
  • Rate limiting is per staff identity, which is why one token per till matters.
  • Secrets in settings are encrypted at rest and are never returned by a read.
  • Revocation is secret rotation, and it is all-or-nothing — every token on the portal dies at once. This is the most significant open item; per-client credentials are a designed, not-yet-built replacement.

The full trade-off analysis, including the ones we have deliberately accepted for demonstration portals and exactly how each is tightened, is not published — it is effectively a map of where to push. Ask for it as part of a deployment review and we will walk it through.

Verifying it works#

A green settings page is not evidence. Neither is a successful deploy. In order:

  1. Ask the app for its own status. GET /hs/serverless/keyring-status on the portal's domain should report bootstrapped, with the schema present and the QR provider you expect. Anything else stops here.
  2. Load the wallet page and the scanner page in a browser. A blank page means the module was provisioned but not wired — a page that renders an empty shell is a different fault from one that 404s, and worth telling apart before escalating.
  3. Mint a staff session for a real venue, with the PIN. A successful mint is the first proof the secret, the venue record and the staff contact all exist and agree — which is exactly what a "complete-looking" settings page does not tell you.
  4. Scan and redeem a test voucher at that venue. Then send the same request again with the same idempotency key and confirm the balance does not move twice.
  5. Attempt a redeem at a venue that lacks the capability and confirm it is refused. If it succeeds, the capability model is not doing its job and nothing above it can be trusted.
  6. Send the loyalty email to a test contact through the send preview and confirm the balances block and the QR both render.
  7. Scan the QR from that email with the staff scanner and confirm it identifies the contact.

Steps 4, 5 and 7 are the ones that get skipped, and they are the three that prove the system rather than the deployment.

Reinstalling — what survives#

  • Uninstalling or reinstalling the app deletes no CRM data. The objects, settings records and existing vouchers survive; the bootstrap carries upgrade logic for schema changes.
  • A scope change requires a reinstall, and until it completes the affected functions fail rather than degrade.
  • Configuration that lives outside the app's own settings does not automatically come back. Before any reinstall, record the current configuration — provider selections, secrets you pasted in, and any per-portal values. A shared secret that only ever existed in a settings record is unrecoverable if the record is lost, and there is no way to re-derive it.
  • CRM cards only reflect a new build after the reinstall picks it up. A card showing old behaviour after a deploy is usually this, not a caching problem.

When something misbehaves#

The failures that have actually occurred during installs on our own portals:

SymptomCauseFix
Every value endpoint returns 503 NOT_BOOTSTRAPPEDThe one-call bootstrap has not run on this portalRun it
Wallet or scanner page renders blankThe page exists but is not bound to the moduleRe-provision the page; a blank render and a 404 are different faults
QR image is blocked inside a HubSpot CRM card, but fine on the websiteThe QR host is not on the app's image allow-listAdd the host and re-upload the app
The loyalty email's balances block is emptyProgrammable email is not enabled on the module, or you are previewing in the editor rather than the send previewEnable and publish the module; preview as a specific contact
The Keyring workflow actions are missing from the pickerThe automation scope was added after the last installReinstall and confirm no outstanding scope changes
Staff sign-in is refused although the details are rightA PIN is configured and not being sent, or sign-in is blocked because no PIN is setCheck both settings — they fail in opposite directions
A redeem is refused at a venue that should workEither the venue lacks the capability, or the voucher is restricted to named venuesThe refusal names the venues where it would work — read it
Configuration looks complete but every token mint failsA setting names a record that has since been archived. Storing an id does not check the record still existsRe-point the setting and use the connection test, which mints a real session rather than checking for non-empty fields

That last row is worth generalising past this app: a settings screen that validates presence rather than reachability will report healthy right up to the first real request. Test by doing the thing, not by checking that the box is filled.

Removing it#

  • Archive the venue records to stop staff signing in at them — capability checks fail closed.
  • Rotate the secret to invalidate every outstanding token immediately, including any till's.
  • Uninstalling the app removes the surfaces and leaves the CRM data. If the data must go too, that is a separate deliberate deletion, and worth deciding consciously — a transaction ledger is often the thing a business most wants to keep.