0050. Channex environment per workspace and the certification evidence pack
Questi contenuti non sono ancora disponibili nella tua lingua.
Context
Section titled “Context”Channex runs two installations: production (https://app.channex.io/api/v1) and staging
(https://staging.channex.io/api/v1, open to self sign-up), each with its own accounts, API keys
and data (the Channex analysis; docs.channex.io, “API
Reference”, read 2026-11-01). Certification takes place on staging. Until now every adapter built
its client for production, so a sandbox workspace (ADR 0049)
reached the production service, and certification could not be scheduled.
Observable acceptance criteria
Section titled “Observable acceptance criteria”- A sandbox workspace connects to Channex staging and a production workspace to Channex production, for every Channex call: connection checks, property discovery and mapping, booking intake, acknowledgements and webhooks, ARI publication, task and value read-back, and Messages.
- Channels shows the environment GuestCraft sends the workspace’s key to, before key entry too.
- A key Channex refuses in that environment is refused with a message that names the environment and its three possible causes. Nothing else is sent anywhere.
pnpm --filter @guestcraft/server certification -- <scenario|all>rehearses Channex’s scenarios against the simulator and writes a reviewable evidence pack; CI runs the same tests.- The booking editor books several rooms in one reservation from the screens (certification scenario 10).
Decisions
Section titled “Decisions”The workspace selects the environment. channexEnvironmentOf in @guestcraft/core/channex
is the one rule: a sandbox workspace uses staging, any other production. It reads the
workspace’s immutable environment column inside the unit of work; nothing derives it from the
deployment (GUESTCRAFT_ENV, preview URLs), from gc_test_ API keys or from the active
organization.
A key travels with its environment. Every Channex port (ChannexRemote,
BookingImportRemote, AriRemote, MessageRemote) takes a ChannexKey: the API key and the
environment that verified it. Core opens a saved key with savedKey, the only place a connection
becomes a key. In @guestcraft/channex, one client factory (channexClient) is used by every
adapter; it passes the key’s environment to the MIT client, whose table is the only place the two
base URLs are written. No adapter builds a URL. Tests and the simulator replace the network
transport, never the URL.
The connection records the environment that verified its key
(channex_connections.environment, migration 0056). It is a contract migration: a previous
runtime would send a staging key to production, so a rollback is refused. A new connection always
takes the workspace’s environment. Every key saved before this change was verified by
production, sandbox ones included, and its webhooks, mappings and Messages installations live in
production. Those rows keep production, which is the truth about the key.
A sandbox key of production fails closed (owner decision, 2026-11-01). The connected key of an
instance can belong to a company account with real client properties, so a sandbox workspace must
never read, write or acknowledge anything there. A connection whose key environment differs from
its workspace’s is in the state environment_mismatch (connectionState); savedKey gives no
key for it, so connection checks, discovery and mapping, booking intake, signals and
acknowledgements, webhook registration, history imports, ARI publication, read-back, drift and
lookup, and Messages (feed, replies, decisions, activation and deactivation) make no Channex call.
The only exception is cleanupKey, used by an unlink alone: it removes GuestCraft’s own booking
and Messages webhooks and its own Messages installation in production, exactly as an unlink does
everywhere. The state shows in GET /channex/connection (state), in sync health
(connection_state), in Channels with the steps to reconnect, and as a sync_failure alert with
the code environment_mismatch on each linked property. To continue, the operator unlinks every
property and waits for the webhook removal, connects a staging key and maps again; a staging key
cannot see the production properties, so a reconnect is refused
(channex.reconnect_requires_unlink) until that cleanup ends. The same key text in the other
environment counts as another key, so pending removals become unconfirmed.
No probing. GuestCraft sends a key only to the workspace’s environment and never falls back
to, or tries, the other one: a sandbox never reaches production. Channex answers 401, 403 or 404
for a key it refuses and does not say why, so PUT /channex/connection answers
channex.key_refused (400, with the environment member) whose text names the three causes: the
key is invalid, has no property access, or belongs to the other environment. GuestCraft cannot
tell which. request.invalid now means only a malformed key.
Webhooks. Booking and Messages webhooks are registered, listed and removed on the
workspace’s Channex environment. The callback is always GuestCraft’s own public
/api/channex/signal/{organization}/{workspace} URL; no Channex host is part of it, and the
signal stays a thin, signed wake-up (send_data: false).
Messages attachments are kept only when their URL is on the key’s own host under /api/v1/
over HTTPS. Another host, the other environment’s included, is dropped and never rewritten.
The simulator models both installations (createChannexSimulator({ environment }), separate
keys and data, channexNetwork routes by base URL). A request to any other origin fails as an
unreachable host. The browser harness rewrites both Channex hosts to two local simulator servers
and refuses any other *.channex.io host.
The evidence pack is collected from the real workflows. The certification rehearsal
(packages/server/test/integration/channex-certification.integration.test.ts) runs on a sandbox
workspace through the public API the web app uses, with the publisher and booking intake as their
jobs run them. A recorder around the Channex transport stores, per scenario, the operator’s
requests to GuestCraft and every Channex call: URL with its host, payload, status and the task IDs
Channex answered. It never reads request headers or Channex’s answers beyond task IDs, and
replaces api_key, webhook headers and booking guest values. A test in the same file fails
if the pack contains the key, the webhook secret, guest data or a secret header name, if any call
reached production, or if two writes to one endpoint were less than six seconds apart. The pack is
simulator evidence. Channex’s certification still runs from GuestCraft’s screens on staging,
with the task IDs of that session; the runbook is in
the certification guide.
Scenario 10 from the screens. The booking editor duplicates a room with its type, dates, occupancy and quote, and stops at the API’s 20 rooms with a visible reason. The rehearsal books the scenario as one reservation of five Twin rooms and a Double room on other nights, which Channex receives as one availability call.
Consequences
Section titled “Consequences”The staging prerequisite of certification is met; scheduling the session, the staging account,
and production credentials with their subscription remain the owner’s actions. The connect
refusal moved from request.invalid to channex.key_refused (API 6.8.0). Occupancy-based rates
are rehearsed in today’s per-room format; when they are published in Channex’s per-occupancy
format the rehearsal is run again.
Not detectable: which environment a refused key belongs to, and whether an accepted production key belongs to an account kept for testing. A production workspace connected to a test account, or a sandbox connected to a staging account that mirrors real properties, behaves as configured.