Skip to content

0050. Channex environment per workspace and the certification evidence pack

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.

  • 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).

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.

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.