ADP — Architecture
Developer reference for functions/modules/adp/. Companion docs: flows, operations.
Surface
One Cloud Function, adp, serving a Hono router. Production host is us-central1-pivot-inc.cloudfunctions.net/adp.
| Route | Auth | Purpose |
|---|---|---|
/subscription/order | Marketplace Basic | Link a company on subscribe |
/subscription/change | Marketplace Basic | Edition change |
/subscription/cancel | Marketplace Basic | Unlink and delete ADP state |
/subscription/notice | Marketplace Basic | Suspend / reactivate |
/subscription/assign | Marketplace Basic | Record a user assignment |
/subscription/unassign | Marketplace Basic | Record a user unassignment |
/subscription/verify | public | Checkout custom-field validation |
/subscription/status | user | Integration status |
/authenticate-sso | public | OIDC code exchange, mints a Firebase custom token |
/export | user | Send a payroll period to ADP |
/search-employees | user | Search ADP workers by name |
/get-payroll-group-codes | user | List payroll groups |
/get-earning-codes | user | List earning codes |
/get-credentials-after-consent | user | Re-fetch client credentials after consent |
Background: onInitAdpIntegration (RTDB trigger) and syncAdpEmployeesCronJob (scheduled).
Subscription event URLs must be registered with the ?eventUrl={eventUrl} template. Without it the handler receives no event URL, treats the call as ADP's validation probe, and returns 200 without acting.
Module layout
Standard modules/<name>/ clean architecture — types/, logic/, contracts/, handlers/, repositories/, services/, endpoints/, wired in container.ts. Handlers take dependencies as a factory argument and return {status, body}; only container.ts imports concrete implementations.
Credentials
Five sets, and two of the secret names do not match ADP's terminology:
| Secret | Holds | Used by |
|---|---|---|
ADP_CLIENT_ID_SSO / ADP_CLIENT_SECRET_SSO | ADP's End-User / SSO credentials | /authenticate-sso code exchange |
ADP_CLIENT_ID_CONNECTOR / ADP_CLIENT_SECRET_CONNECTOR | ADP's Data Connector credentials | credentials.read |
ADP_SUBSCRIPTION_CLIENT_ID / _SECRET | Inbound Marketplace credentials, issued by ADP | Fetching subscription payloads |
ADP_SUBSCRIPTION_USERNAME / _PASSWORD | Outbound Basic auth, chosen by us and typed into the listing | Verifying inbound marketplace calls |
ADP_PUBLIC_CERT / ADP_PRIVATE_CERT | mTLS client certificate and key, base64 of the PEM | Every ADP API call |
Per-client credentials are fetched once via credentials.read and stored per company. A 200 response with an empty body means the client has not consented.
Data model
AdpSettings/{companyId}
organizationOID ADP organization, the join key for SSO and events
clientId / clientSecret per-client API credentials
payrollGroupCode export target; writing this fires the initial sync
assignedUsers/{associateOID} { status, updatedAt, email, name }
CompanySettings/{companyId}
hasAdpIntegration subscription linked
hasAcceptedADPConsent credentials retrieved
adpSuspended set by SUBSCRIPTION_NOTICE
EmployeeIntegrationIds/{companyId}/adp/{employeeId}
{ id, payrollFileNumber } id is the ADP associateOID
associateOID is the canonical person identifier and the key everything joins on. ADP's assignment payload carries it at payload.configuration.associateOID — payload.user.uuid is an OpenID subject and is not the same value.