Skip to content
Skip to main content
Novel Systems home
Decision log
D-065August 5, 2026

A public endpoint says which connectors this deployment can actually begin

Decided

GET /api/connectors — unauthenticated, outside /v1, alongside /health and /routes — reports one of three derived states per provider, and /integrations renders it on the same page as the field mapping table.

Affects: backend/src/routes/index.ts, backend/src/schemas/common.schema.ts, backend/scripts/generate-openapi.ts, backend/test/api-integration.test.ts, lib/connector-readiness.ts, app/integrations/page.tsx, scripts/check-api-reference.mjs. Extends D-012 and stands beside D-045 rather than replacing it.

The gap this closes. /integrations publishes, for each of the three system-of-record connectors, a field mapping table, an auth method, a cadence and a declared system of record. All of it is true about the code: the adapters exist, hstCents is unit-tested to the cent, credentials are sealed before they touch a row, and 265 tests run against the integrations layer. None of it answers the question a buyer is actually asking, which is whether pressing Connect today reaches a consent screen or reaches nothing. On this deployment, for QuickBooks and Salesforce, it reaches nothing — no Intuit app and no Salesforce connected app exist yet.

D-012 drew that boundary honestly and stated it in three places: the architecture diagram, the admin endpoint, and the header of the integrations test file. All three are places a buyer never looks. This is the fourth, and the first one on the public side of the guard.

Why an endpoint and not three lines of copy. Three lines in lib/integrations.ts would render identically today and would be wrong the morning after somebody sets QUICKBOOKS_CLIENT_ID in Vercel — wrong in the flattering direction, which is the direction nobody audits. The state is derived from describeProviders(), which reads the adapters, which read the environment. There is no table to maintain and therefore none to forget.

Why this does not supersede D-045. The maturity field still says "Generally available" for all three, and that is still correct. Maturity answers *what shape is the work* — a switch, not a project. Readiness answers *can I flip the switch here, today*. They are different questions with different lifetimes, and collapsing them would lose the one that is durable.

What is deliberately withheld. No tenant rows, and not the environment variable names. gap.summary on the admin endpoint reads "QuickBooks needs QUICKBOOKS_CLIENT_ID and QUICKBOOKS_CLIENT_SECRET in the API environment" — correct and useful behind requireAdmin, and on a public endpoint an inventory of unset secrets. So the public response uses a separate sentence per state, written for a buyer. Whether a Connect button would work is a fact anyone can establish by pressing it; the name of the variable holding a client secret is not. Two tests assert the leak cannot reappear.

Why the test environment is the inverse of production, and why that is the point. test-env.ts sets the QuickBooks and Salesforce client credentials and deliberately withholds STRIPE_SECRET_KEY — setting it would invoice a real card on every approval in the suite. So the endpoint must report Stripe as the unconfigured one and the other two as ready, exactly backwards from the deployment. Any hardcoding fails in CI rather than in front of a buyer.

Considered and rejected: render nothing until Intuit and Salesforce apps exist. That leaves a page making three claims with nothing behind two of them for however long the signups take, which is the same defect in a quieter form. The uncomfortable output is the correct one: a worse-looking page and a truer one.

This would be wrong if the endpoint ever grew a fourth state that the site could not render, or if a provider's readiness stopped being derivable from its adapter. scripts/check-api-reference.mjs re-derives the three states from public/openapi.json and fails the build in both directions — a state in the spec the reader cannot render, or a branch in the reader the API never returns. Both failure modes were exercised before the check was committed.