Corteksa
GuidesIntegrations

Backend Integration

Integrations — Backend Integration

For backend engineers extending or maintaining the external developer surface — API-key auth, scopes, and the OAuth "Connect" server. Everything below lives under src/api/v1/developers/ (plus the guard in src/common/guards/).

One gate for humans and apps

Every request — a logged-in admin or an external app — enters through a single guard, ApiAuthGuard (src/common/guards/api-auth.guard.ts), which extends AdminAuthGuard. It classifies the credential by prefix (classifyCortexaToken, api-key.util.ts) and routes it:

CredentialPrefixHandler
API keycrtk_live_…ApiKeyAuthService (api-key-auth.service.ts)
OAuth access tokencrtk_oauth_…OAuthTokenAuthService (oauth/oauth-token-auth.service.ts)
Human JWT(none of the above)super.canActivateAdminAuthGuard unchanged

Every branch attaches the same acting-as-admin AdminUser, so the downstream PermissionGuard and row-level filters behave identically regardless of who authenticated.

The API-key auth path

ApiKeyAuthService.authenticate:

  1. extractApiKey(req) — reads X-Api-Key or Authorization: Bearer crtk_….
  2. ApiKeysService.findByRawKey(raw) — hashes the key and does an O(1) by-hash lookup; returns null for unknown / revoked / expired keys → 401.
  3. ApiKeyRateLimiter.check(key.id) — per-key fixed window → 429 if over.
  4. Stamps req.apiKeyId = key.id (so the webhook fanout can skip echoing the key's own writes), then hands off to TokenPrincipalService.establish.

TokenPrincipalService.establish (token-principal.service.ts) is the shared projection used by both the API-key and OAuth paths, so acting-as-admin behaviour can't drift:

  • On hyper, pins the RLS connection to the credential's workspace_id (pinWorkspaceConnection); a hyper token with no workspace fails closed.
  • Builds an AdminUser that acts as admin_id, with isSuperAdmin: false (tokens never inherit the super-admin bypass) and permissions = expandObjectScopes(scopes, snapshot.routeNames).

Enforcement is two layers

  1. Endpoint gate (coarse): PermissionGuard + @RouteName('{verb}.{slug}') checks the credential's scopes — the same mechanism as a human's permission list. A call outside the granted scopes → 403.
  2. Row-level scoping (fine): even past the gate, the record services re-check the acting admin's current A/G/M/D level per record (access-filter.builder.ts). A credential's data access can never outlive the admin's rights — the scope list is only the gate.

See Authorization for the A/G/M/D model.

The scope model

Scopes are route-name strings: {verb}.{objectSlug}read.contacts, create.deals, update.contacts, delete.contacts. Two rules, both in object-scope.util.ts:

  • Wildcards{verb}.* (e.g. read.*) grants a verb across every object the owner can reach. expandObjectScopes resolves it at auth time against the owner's live object gates (never more), so a broad grant stays bounded.
  • Grant checkisScopeHeld gates what a key/app may be granted. A key can never carry a scope its creator doesn't hold; ApiKeysService.resolveScopes enforces requested ⊆ creator.permissions (super-admins may grant any).

Secret storage & hashing

TokenHashService (token-hash.service.ts) is the single owner of how the platform hashes every secret — API keys, OAuth client secrets, access/refresh tokens, and auth codes. All are ≥256-bit random, so a deterministic HMAC-SHA256(pepper, raw) is safe and O(1)-lookup-able. Pepper = API_KEY_HASH_SECRET (falls back to JWT_SECRET). Only the hash is stored — a DB dump yields nothing usable (see ApiKey, api-key.entity.ts).

The OAuth token flow (server-side)

The token endpoint (POST /oauth/token, oauth/oauth-token.controller.ts) is public — the client authenticates, not a user (@SkipAllGuards, @SkipTransform so the body is raw RFC 6749 JSON). OAuthTokenService.exchange (oauth/oauth-token.service.ts) handles two grants:

GrantVerifiesResult
authorization_codesingle-use code (Redis, 60s), exact redirect_uri, PKCE code_verifier (public) or client_secret (confidential)new access + refresh pair
refresh_tokenclient_secret (confidential), live refresh rowrotates — revokes old, mints new pair

Details worth knowing:

  • Opaque + revocable. Access tokens are crtk_oauth_… (1h TTL, ACCESS_TOKEN_TTL_SECONDS), refresh tokens crtk_refresh_… (30d). Revoking a grant/app kills live tokens immediately, not at expiry.
  • Refresh rotation + reuse detection. A presented refresh token that was already rotated is treated as a leak (OAuth 2.1): detectRefreshReuse revokes the whole token family.
  • Token writes run inside TenantScope.runInScope — the rows are workspace-scoped, so writes from the public endpoint pin (and release) the RLS connection, exactly like the Gmail OAuth callback.

Access-token authentication (a partner calling the API) is the OAuth twin of the API-key path: OAuthTokenAuthService.authenticate does an unpinned by-hash lookup, checks revoked/expired, stamps req.oauthClientId, and calls the shared TokenPrincipalService.establish.

Where to look

ConcernFile
Single credential gatesrc/common/guards/api-auth.guard.ts
Key classification / extractiondevelopers/api-key.util.ts
API-key authdevelopers/api-key-auth.service.ts
Key mint / revoke / lookupdevelopers/api-keys.service.ts
Acting-as-admin projectiondevelopers/token-principal.service.ts
Scope wildcards & grant checkdevelopers/object-scope.util.ts
Secret hashingdevelopers/token-hash.service.ts
Per-key rate limitdevelopers/api-key-rate-limiter.service.ts
OAuth token exchangedevelopers/oauth/oauth-token.service.ts
OAuth token authdevelopers/oauth/oauth-token-auth.service.ts
Consent (authorize)developers/oauth/oauth-authorize.controller.ts
App registry (super-admin)developers/oauth/oauth-app.controller.ts
Module wiringdevelopers/developers.module.ts

Module wiring & multi-tenant safety

DevelopersModule is @Global()ApiAuthGuard is applied per-controller across the app (records, objects, fields, webhooks), so its two dependencies (ApiKeyAuthService, OAuthTokenAuthService) must resolve everywhere. Opening a new controller to apps is a one-line guard swap, no imports: wiring. Those two services are the only exports — everything else stays encapsulated.

All DB access flows through the pinned ContextAwareRepositoryProvider. The api_keys and OAuth token tables carry workspace_id under a login-aware RLS policy — permissive only for the by-hash auth lookup (before a workspace is known), strict once pinned. Never reach past the pinned connection to the root DataSource.

Next

On this page