One shared TSANet-facing core with thin per-platform adapters, for members whose platform has no native connector. This is the machinery behind Fin / Intercom and the gateway-class connectors that follow it — and the contract a member or platform vendor builds against to write an adapter of their own.
Status
Members on Salesforce, Dynamics and Zendesk install a connector into their own tenant. Members on platforms that will not host member code — Intercom today; Pylon, DevRev and the like are the obvious candidates — need the integration to run somewhere else. The Connect Gateway is that somewhere: a TSANet-facing engine that owns everything on the Connect side, embedded by a thin adapter that owns everything on the platform side. Why a platform lands in this class at all is a property of the platform, not a choice — the connector classes topic has the procedure.
The engine was not designed on paper. It was built and verified end to end inside the Fin / Intercom connector first, and extraction into its own repository deliberately waited until a second platform committed — so the module boundary was fixed by two real consumers rather than one and a guess. The Fin connector now consumes the published engine artifact like any other adapter.
Where it stands: the contract, the engine, the conformance harness and the worked example are real and published. The current release is v2.0.0 (2026-09-09): the engine takes connect-library 2.0.0, and all four modules (connector-core, conformance, gateway-engine and the parent) publish at 2.0.0. It is an engine change and nothing else: the TsanetGateway contract and the conformance suite are unchanged from v1.0.0 (the connector-core and conformance code and poms are identical between the two tags), so an adapter that compiles against 1.0.0 compiles against 2.0.0 unchanged. What moved is login handling: a failed login is classified and never ambiguous, and a stale token is renewed in a login lane before the operation, under its own budget rather than the operation's deadline. The runtime line v1.0.0 set (2026-08-28), Spring Boot 4.1.1 and Jackson 3, is unchanged. Around that core, plenty is still moving; the last section is honest about what.
v1.0.0 shipped while the repository README and the adapter-author onboarding still named v0.1.2 as the certification pin. The repository closed that on 2026-09-09. The member-facing documents were moved to the published version, v1.0.0 was re-certified with a live contract run against Beta (9/9), and the publish workflow now refuses to publish a version those documents do not name, so the label cannot fall behind the artifacts again. v2.0.0 was cut under that gate, documents first, and re-certified the same way on the engine tree it tags. Consume 2.0.0; the repository's pin language is current.tsanetgit/Connect_Gateway) is currently private. Adapter authors get access at onboarding, along with the published artifacts and a self-contained authoring kit — start with membership if that is you.Design
The design rests on a single asymmetry: TSANet knows the Connect API and its failure modes; the platform owner knows the platform. So the gateway owns the whole TSANet side — authentication, per-member sessions, partner search, process forms, submission, reconciliation, deadline enforcement, and the classification of every failure — and the adapter owns the platform side in both directions. Adapter code never implements TSANet semantics: it never decides when a submission may be retried and never interprets a Connect error. That rule is what makes it safe for someone who is not TSANet to write an adapter, because the part that pages real engineers at real partner companies is not theirs to get wrong.
The same split, at estate scale: every platform lands as one thin adapter over the same contract, and everything to the right of the contract is shared — one session registry holding per-member credentials, one engine implementation, one path to the Connect API.
The TsanetGateway contract, its DTOs and its two exception types. Zero dependencies, compile-scope — your adapter's own code is written against it.
The TSANet-facing engine an adapter embeds: the contract implementation over the Connect API, the per-member session registry, and a Spring configuration the embedding service imports. Configuration under gateway.tsanet.*; the engine wires itself only when gateway.tsanet.mode=sdk is set, and without it the application starts with no gateway bean, which is the right state for tests against the conformance double.
The harness an adapter author runs to prove their adapter survives Connect's failure modes, plus the live certification suite the certification pin was cut against.
A complete runnable worked example: adapter over the contract, in-memory platform, conformance suite green, and a bootable service embedding the real engine. Fake mode runs the whole story with zero credentials.
There is also a TSANet-internal admin console (read and validate, not operate) over a versioned admin contract that TSANet-hosted adapters expose; member-embedded adapters are explicitly outside its scope, so monitoring and on-call for those stay with the author. Three of the modules above — connector-core, gateway-engine and conformance — ship from the repository's package registry together with the parent POM, at the release version: 2.0.0 since 2026-09-09, alongside the older 1.0.0 and 0.1.2. The examples and the admin console are source-only, built from the repository rather than published. (Verified against the registry's package list on 2026-09-13.)
Contract
The surface is deliberately small. Outbound: searchPartners, form (fetched fresh per submission, never cached), submit, and findSubmission for reconciling by your own case number. Inbound: fetchByToken and listInboundUpdatedSince — the poll behind the push on a TSANet-hosted adapter, and the whole inbound surface for a member-embedded one, since there is no push channel from TSANet to a member-embedded adapter and the poll interval is its inbound latency bound — plus the three response actions an adapter calls when its own user answers — approve, reject and requestInformation, each taking the request token and the responding engineer's identity (approval also carries the case number and next-steps text, as the Fin sequence shows). Files: forwardAttachments outbound, and an AttachmentDeliveryHandler seam for inbound delivery. One more, healthy, for readiness. The contract has no webhook seam: as of v2.0.0 the TSANet push — signature verification, CloudEvents parsing, routing — is terminated by the adapter itself (Fin / Intercom's TsanetWebhookController), so the next adapter would carry that too until the engine takes it on.
The centre of the design is the exception model. Submitting a collaboration case is not idempotent and there is no idempotency key — a submit that fails may still have created a case at your partner, and a blind retry pages a second real engineer. So the contract splits every failure into exactly two kinds:
| Exception | Meaning | What you do |
|---|---|---|
GatewayException | Definitively rejected — no case exists or can exist | Nothing was created: show the problem detail next to the field that fixes it, and let the user correct and submit again — a blind retry cannot succeed |
GatewayTimeoutException | Outcome unknown — deadline expiry, 5xx after send, reset mid-response, or a 200 with no token | Park it; reconcile with findSubmission before any resubmit |
GatewayTimeoutException extends GatewayException. A single catch of the parent silently treats uncertainty as rejection — the exact failure the whole model exists to prevent:
// Wrong: also catches GatewayTimeoutException — ambiguity treated as rejection
try { gateway.submit(draft); }
catch (GatewayException e) { giveUp(); }
// Right: subclass first, and the two arms do different things
try { gateway.submit(draft); }
catch (GatewayTimeoutException ambiguous) { park(internalCaseNumber); }
catch (GatewayException rejected) { surfaceToUser(rejected); }
The engine does the classification — a parsed 4xx, a timeout, a connection reset all arrive pre-sorted — so adapter code never inspects an HTTP status. Carry your own case identifier through retries unchanged: it is your only means of reconciling, and generating a fresh one on retry destroys it.
Adapters
Two audiences, split by one question: can TSANet reach the platform? Where it can — a multi-tenant platform with a public API — TSANet builds and operates the adapter, the way Fin / Intercom shipped. Where it cannot, which is the case whenever the target is a member's own internal system, the member or vendor writes the adapter themselves against a self-contained kit: the contract, the engine artifact, the conformance harness, and an authoring guide that assumes no access to TSANet's tooling. The split is not cosmetic — one audience can probe the platform and one cannot.
v2.0.0, connect-library 2.0.0, and the vendored spec at a named beta-branch commit — with the rule never to bump any of them yourself. Reaching the private registry needs a classic PAT with repo as well as read:packages; the published artifacts are BSD-2-Clause.testSubmission and read testCase, and that read-back is the receipt.sqlite-path per deployment; two writers on one store is undefined. The store is not the token-to-case routing table. Credential rotation is configuration plus a full restart, not a rolling update. The bounded startup health probe never fails boot and logs healthy=false instead; tsanet_gateway_timeout and tsanet_gateway_unhealthy are the two log keys to watch.Everything it checks exists because submission is not idempotent. The first two rows together are an exactly-once property — checking only for duplicates would pass an adapter that drops every ambiguous case.
| Check | The failure it prevents |
|---|---|
| Ambiguous submit that did create the case: exactly one case | A blind retry sends a second case to the partner |
| Ambiguous submit that did not create the case: exactly one case | Giving up on ambiguity silently loses the request |
| Two ambiguous submits in a row: still exactly one case | A retry path that is correct once and blind the second time |
| A reconcile that itself fails is not read as "absent" | The subtlest duplicate: a lookup that threw tells you nothing |
| Reconcile precedes any resubmission | Separates being correct from being lucky |
| A definitive rejection creates nothing | Retrying an invalid request cannot succeed |
| The process form is fetched fresh per submission | A cached form submits fields the partner has deleted |
The harness only sees the TSANet side of your adapter, because TSANet has no access to your platform. Your inbound path, your platform writes actually landing, credential isolation (one client per member, never shared), and test-flag discipline are yours to test.
The authoring kit is written as an agent skill, deliberately: a SKILL.md that carries the build order and the rules, plus reference files it points into — the platform questions, the binding semantics, onboarding, deploy-and-go-live, and the vendored API spec. The spec is vendored because the specification's own repository is private and deliberately not granted; the copy is pinned to a beta-branch commit, byte-identical to it on 2026-09-13, and behind the main-branch spec by the webhook subscription filters (caseDirections, partnerCompanyIds) and a few field lengths. One reference had drifted the other way: the deploy-and-go-live file named Spring Boot 3.3.4, where the v2.0.0 engine pom, which it calls authoritative, says 4.1.1; that was corrected in the kit on 2026-09-14. Loaded into an agentic assistant, it makes the assistant apply TSANet's rules while it writes your adapter, instead of you relaying them.
When onboarding grants you repository access, copy the whole kit folder — the references must ride along with the SKILL.md that cites them:
cp -R Connect_Gateway/skills/connect-adapter-author your-adapter-project/.claude/skills/
That path is Claude Code's project skills directory; for other assistants, attach SKILL.md and the references/ files as project instructions — the kit is plain markdown and assumes no particular tool. (A second, TSANet-internal skill covers the adapters TSANet builds itself; it is not part of the member kit.) Every skill TSANet ships, and how to load one into any assistant, is collected on Agentic usage with TSANet Connect.
Typical first steps, in the kit's own order:
Attachments
Case files get their own set of gateway-wide decisions, made ahead of any consumer because the first runtime that references a configuration key freezes it:
The platform-wide attachment model — no store, no listing, two-party configuration — is specified in TSANET-2026-002; the gateway decisions above are the gateway's answer to it.
Field lessons
The gateway repository keeps a lessons-learned log with a deliberate promotion path: something seen on one platform is a lesson, something seen on two is a rule, and rules move into the adapter kits where they load automatically. The entries below are the ones that matter to anyone building an adapter, each tagged with where it was seen — the same probed-versus-read discipline the platform pages use.
On the Intercom spike, the vendor's own documentation was wrong or silent on the majority of the findings that shaped the design — the response deadline, the signature byte format, the behaviour on unknown fields. Reading produces a plausible design; probing produces a correct one. Budget the spike accordingly, and tag every claim with whether it was probed or merely read.
Seen on: Intercom, consistent with Zendesk
The opposite failure also costs real time: two facts that shaped the Zendesk connector — integration names are globally unique, the OAuth client is auto-created — were documented on a single page nobody had read. Probing is right when documentation is silent or suspect; reading first is right when nobody has read it yet.
Platform-independent
Zendesk's "Add Tags" endpoint replaces the tag set — and because other fields were tag-backed, the first write also blanked an unrelated status field. The failure is invisible unless you look at what was there before, which is why it ran in production before anyone noticed. Where a mutation's name implies a merge, read the object before and after.
Seen on: Zendesk
Intercom returns 200 for a write carrying a field name it does not recognize, and ignores the field — so every downstream bug looks like application logic rather than a wire-format mistake, and you debug the wrong layer for hours. Probe for this deliberately on every new platform: send a plausible-but-wrong field name and read the object back. Where it is true, read-back-and-assert becomes a standing rule for that adapter.
Seen on: Intercom · unverified elsewhere
An authentication page showing every permission enabled does not subscribe a single webhook topic — topics are selected separately, and an empty topic list delivers nothing, silently, presenting exactly like a broken endpoint or a bad signature. When a webhook never arrives, check the subscription list before debugging the receiver.
Seen on: Intercom · the same split exists on several platforms
A probe that cannot report the negative case is measuring nothing — the estate has seen "clean" verdicts from checks that were scanning nothing at all. Before trusting a probe that returns clean, include a control you know it should catch, and check the exit status of the command you actually care about rather than the last one in a pipeline.
Platform-independent · seen repeatedly
The Connect client library worked perfectly inside Spring Boot and failed time serialization everywhere else — Boot was invisibly supplying a Jackson module the library needed. The gateway extraction's standalone certification run caught it: three of nine live contract tests failing outside Boot, the exact defect a member embedding the engine would have hit on day one. Certify an embeddable artifact outside its reference framework, and when a failure appears or disappears with the harness, suspect the classpath before the code.
Caught extracting gateway-engine, 2026-08-09
During a Beta-estate webhook dispatch outage, events from a thirteen-hour window were permanently lost — nothing replayed on recovery, and the one live case created during it arrived only because the reconciliation poller listed it on the next cycle. The poller's cursor rests on a probed fact (a freshly created case returns updatedAt equal to createdAt, so an updated-after filter catches creations); had that been assumed and wrong, recovery would have missed exactly the new cases it exists to catch. Never build inbound on push alone: reconciliation is the source of truth, push is the latency optimization.
Seen on: TSANet Beta, 2026-08-09
An annotation that partly succeeded, recorded as a boolean "done", reports as fully done — and the manual recovery path stands on that record being honest. Wherever a write can partly land, the record needs at least three states; the Fin adapter's is none / degraded / full, with the degraded case audited loudly.
Platform-independent
A container template that references its image by tag rolls nothing when a rerun pushes genuinely new code: the platform never re-resolves a same-tag reference. Every check stays green and the old code keeps running. Pin by digest or SHA anywhere a deploy would otherwise re-resolve nothing — a git dependency pinned by branch name is the same class of mistake.
Seen on: Azure Container Apps
Across every review round on the first adapter, the blocking findings sat in whatever had been built most recently — and none were visible to unit tests. Review the newest seam hardest, and get the review from someone other than the author, because reviewing your own diff has a blind spot that care does not close.
Platform-independent
A merged pull request ships nothing on its own where a build artifact carries its own version. Check the tag against the manifest after every merge that touches shipped assets.
Seen on: the Zendesk connector, more than once
Work in progress
| Area | State |
|---|---|
| The contract, its exception model, the conformance checks | settled — unchanged through v2.0.0; certified against a live Beta run (9/9) at v2.0.0 on the same sources |
| The engine and the worked example | settled — published artifacts at 2.0.0; the Fin connector consumes the published engine like any other adapter and is on 1.0.0 until its next bump |
| The runtime line | moving — v2.0.0 runs Spring Boot 4.1.1, Jackson 3 and connect-library 2.0.0, all in a tagged release; still open is moving the generated API calls onto the library's own HTTP client so every failure is one type |
| Adapters in production | one — Fin / Intercom, in early access; patterns have not yet been stress-tested by a second shipped adapter |
| Cross-integration conventions | converging — the authoring kit keeps an explicit "not settled" list; those questions go to TSANet rather than being answered locally |
| Attachment hosting | deciding — self-service credential intake is the target state, not the day-one state, and whether TSANet ever offers production file storage is an open business decision |
| Hosting and sizing of the shared runtime | proposal stage — the production topology is designed, not yet operated at member scale |
This page is deliberately ahead of the estate it describes — that is what the work-in-progress marker means. Expect it to change as the second and third adapters land, and read the Fin / Intercom page for what operating the first one actually looks like.
Verified against tsanetgit/Connect_Gateway at the v2.0.0 release (2026-09-09): the release notes, the published package versions on GitHub Packages, the module poms at the tag, the repository README, the conformance harness documentation, the adapter-author onboarding kit and the publish workflow's docs gate. The pin-label drift recorded in earlier revisions of this page was confirmed closed by reading the README at the tag and the gate step that enforces it. The design material this page draws on (the gateway decision log, the wiki's lessons-learned log of 2026-08-13 and the as-built architecture reference of 2026-08-13) was last checked against main on 2026-09-08 by the previous revision of this page.