Webhook signatures
Fluid signs every webhook it sends. The template’s/api/webhooks route checks the signature before it does anything else.
- Fluid sends
X-Fluid-Signature, an HMAC-SHA256 of{timestamp}.{raw body}, andX-Fluid-Timestamp. - The template rejects a webhook whose timestamp is more than five minutes old, to block replays.
- Company webhooks, such as
order.created, are checked against that company’s own webhook secret. The template finds the company from theX-Fluid-Shopheader. - Lifecycle webhooks,
droplet.installedanddroplet.uninstalled, are checked againstFLUID_WEBHOOK_AUTH_TOKEN, which Fluid sets when you add a droplet.
webhooks table, with tokens and secrets removed from the stored copy. See Webhooks for the events Fluid sends.
The droplet install flow
When a company installs your droplet, Fluid sendsdroplet.installed to /api/webhooks/installed. The template then:
- Exchanges the short-lived install token for the company’s credentials: a droplet installation token, which starts with
dit_, and a webhook secret. - Saves them in the
companiestable, one row per installation. - Registers the webhooks, callbacks and drop zones you turned on in
lib/config/droplet.config.ts.
Installation context for embedded pages
When the Fluid admin opens your droplet, it adds?dri=dri_… to your app’s URL. That value identifies the company’s installation of your droplet.
The template turns it into a server-side tenancy boundary:
- In the browser, read it once with
readFluidInstallationReference(window.location.href). - Call your own API routes with
fluidInstallationFetch(). It sends the reference in theX-Fluid-Droplet-Installationheader and only to your app’s own origin. - On the server, call
resolveFluidInstallation(request)first in every route that reads company data or calls Fluid. It finds the one active installation, or throws.
context.companyId or context.installation.
Viewer identity with session tokens
The Fluid admin can also add asession_token to the URL. It’s a short-lived token, signed with HS256, that names the viewer (sub) and the store (dest).
verifySessionToken() in lib/fluid/session-token.ts checks it on the server. It checks the signature, issuer, audience, expiry and store, and allows 30 seconds of clock skew. It reads its keys from FLUID_SESSION_TOKEN_SIGNING_SECRET, FLUID_OAUTH_CLIENT_ID and FLUID_SESSION_TOKEN_ISSUER. Fluid sets them in production only, and verification fails closed when they’re missing.
The /embed/session-example route shows the full pattern:
- It resolves the installation from
dri. - It looks up the store with the installation’s token. This needs the
settingsscope on the installation. - It verifies the
session_tokenagainst that store. - It sets its own HttpOnly cookie for 120 seconds, so later pages in the same frame don’t need the token again.
- It removes
session_tokenfrom the browser’s address bar.
What you must not build yourself
- Don’t create your own login. Don’t add a password form, an OAuth flow or a second viewer token for pages embedded in Fluid. Use the installation context and the session token.
- Don’t trust what the browser sends about the company. Never pick the company from a query parameter, the
X-Fluid-Shopheader of a browser request, a shared token or “the only row in the database”. Resolve the installation on the server. - Don’t send tokens to the browser. Keep droplet installation tokens and
FLUID_COMPANY_PRIVATE_TOKENon the server. Never forward a Fluid session token to the Fluid API. - Don’t skip signature checks. Don’t disable the webhook signature check, even while you test.
Local development
Locally, nothing signs webhooks or session tokens for you. The home page renders without a viewer, so you can build the UI right away. To test the install flow locally, copy.env.example to .env.local and set the lifecycle variables it lists. The session-token variables exist only in production, so verified-viewer routes return 401 locally.
Related pages
- Integration points
- Authentication for every Fluid token type
- Creating droplets