tnl.dev :: docs

company connections.

Connect a company directory and manage member admission.

connect a company directory

An organization team owner can connect one company directory at /a/company. Choose Google Workspace, Microsoft Entra, or a GitHub organization. Use the provider's immutable customer, tenant, organization, and group IDs. An email domain does not establish ownership of a tnl team.

Ordinary sign-in uses the hosted shared provider apps. Authorizing directory access is a separate administrator operation. The directory determines who is eligible; a current sign-in with that source determines how someone authenticated. Linking a source account alone does not establish a company sign-in.

Choose owner approves requests or managed member access. Employees sign in with the source and use /a/company-access, selecting a known team by name or following the owner's team-specific request link. The page opens control's approval URL: verify the matching code and complete source sign-in. The page then submits the admission request using that browser-bound control session. Both paths grant only the tnl member role; owners manage roles separately.

Before starting directory setup, sign in recently with the source account that will qualify you as an owner. Setup remains pending while the administrator authorizes directory access. An existing connection stays in effect if consent fails. If saving an activation is interrupted, use finish directory authorization on the owner screen to recover its provisioning permission. Setup saves its encrypted provisioning credential before activation. Retrying uses that same credential, including when control committed but its reply was lost. If the screen asks for source sign-in, sign in again and finish setup. If consent expired before activation started, restart administrator consent instead.

Setup, disconnect, and sync share one operation lease. A concurrent operation asks you to retry; an expired worker cannot overwrite a newer result. After an interrupted disconnect, refresh the screen to confirm whether control removed the connection.

To change provider or Entra tenant, first explicitly link the new source account to the same tnl identity at /a/sign-in. Linking does not change the current sign-in. Sign in with the current source and explicitly disconnect the directory. Company access stops until you sign in with the new source and complete new administrator consent. This transition never enables personal sign-in for the owner or other members automatically.

Google Workspace

  1. In Google Cloud, configure a web OAuth client for directory administration, separately from the ordinary shared sign-in client. Set its redirect URI to https://<account-host>/api/account/company/callback.
  2. Enable the Admin SDK API. Configure consent for admin.directory.customer.readonly, admin.directory.user.readonly, admin.directory.group.readonly, and admin.directory.group.member.readonly under https://www.googleapis.com/auth/. Complete Google's app verification or organization trust requirements for these scopes as applicable.
  3. The hosted operator sets COMPANY_GOOGLE_CLIENT_ID and COMPANY_GOOGLE_CLIENT_SECRET in server-side secret configuration.
  4. In tnl, enter the Workspace customer ID (such as C01234567), choose immutable group IDs, or explicitly choose all users. A Workspace administrator who can read the requested directory data completes Google's consent screen.

tnl requests offline access using authorization-code exchange and PKCE, verifies the authorized customer, and encrypts the refresh token in private storage. Selected groups must belong to the customer. Nested customer-owned groups are expanded; external users do not become eligible through a group. Suspended and archived users are excluded. This uses administrator OAuth consent, not a downloaded service-account key or domain-wide delegation.

Microsoft Entra

  1. Register a multi-tenant directory application, separate from shared sign-in. Add the same account callback URI as a web redirect URI.
  2. Configure Microsoft Graph application permissions: Organization.Read.All, User.Read.All, GroupMember.Read.All, and Application.Read.All. Grant tenant administrator consent. This integration reads directory and app-assignment data; it does not require a SCIM endpoint.
  3. The hosted operator sets COMPANY_ENTRA_CLIENT_ID and COMPANY_ENTRA_CLIENT_SECRET server-side. Rotate the application secret before it expires.
  4. In tnl, enter the tenant GUID. Choose group GUIDs, a service principal GUID whose app assignments select users/groups, or explicitly choose all users. Complete the tenant-specific administrator consent screen.

tnl obtains an application token from that exact tenant and verifies the Graph organization ID. A callback's admin_consent flag alone is not proof of consent. Group selections include transitive user members. App assignments include direct users and transitive users of assigned groups. Disabled users, guest objects, and service principals are excluded. The source sign-in must carry the verified tenant ID and object ID; matching an email suffix is insufficient.

GitHub organization

  1. Register a GitHub App with organization Members: read-only permission. Set its setup URL to the account callback URI. Subscribe to installation, installation suspension, organization, member, and membership events supported by that app's permissions. Set its webhook URL to https://<account-host>/api/company/github and use a separate webhook secret.
  2. The hosted operator sets COMPANY_GITHUB_APP_ID, COMPANY_GITHUB_APP_SLUG, COMPANY_GITHUB_PRIVATE_KEY (PKCS#8 PEM), and COMPANY_GITHUB_WEBHOOK_SECRET in server-side secret configuration.
  3. Sign in to tnl with GitHub recently, enter the immutable numeric organization ID, and authorize the GitHub App installation as a current organization owner.

tnl verifies the installation's organization ID, membership permission, and unsuspended state, then verifies that the initiating account is an active organization administrator. Organization names are used only after resolving the installation's immutable account ID. Pending invitations do not qualify: only current active organization membership does. GitHub App installation tokens are short-lived and remain server-side. Webhooks are HMAC-verified and deduplicated; they schedule a full reconciliation rather than directly removing people.

admissions, contractors, and departures

Owners approve or deny individual requests on the connection screen. Managed admission grants member access after a verified eligible source sign-in. A manually removed membership is not automatically restored by either path.

An owner can explicitly allow a particular contractor invitation or membership to use personal sign-in. The checkbox defaults to off and requires its exact ID. Directory updates never clear that policy. It does not give other employees a personal sign-in exception.

Manual suspension is reversible and independent of role or personal sign-in policy. Removing an identity from directory eligibility suspends its member-owned access until eligible again; sync never restores a manually removed membership. Employee suspension stops member-owned access and publish runs. Team-owned unattended publishers survive their creator leaving; sync does not change their team ownership or tnl roles.

sync and freshness

The hosted scheduler calls GET /api/cron/company-sync (or POST) each minute using the existing cron service credential. Healthy connections are scanned every five minutes. Eligibility lasts at most fifteen minutes from the start of a successful scan. The owner screen shows the last complete scan and its eligibility deadline. It refreshes each minute and when returning to the tab. Refreshing directory state does not renew authentication: use the prompted source sign-in when the proof is too old. GitHub signed webhook deliveries can wake a full scan; Google and Entra use periodic reads, without a push-notification promise.

Every provider page must finish successfully before employee eligibility updates are submitted. Control receives individual updates for mapped accounts, including former eligible accounts that disappeared from the completed scan. Mapping and delivery progress is saved in chunks of at most 1,024 identities, with at most 32 authority requests in flight. Large reconciliations resume on later scheduler calls with the same snapshot deadline and exact update revisions. A lost reply replays the stored update; resuming does not add fifteen more minutes. Failures and rate limits preserve the prior snapshot without extending its deadline. Detected revoked authorization prevents new website company proofs; control enforces the previous eligibility deadline. Scans are bounded to five minutes, 50,000 users, 1,000 pages per collection, and 1,000 nested Google groups. A source above those limits needs a narrower selection or a larger integration designed for that directory; a truncated scan is never treated as complete.

Provider directory reads are not transactional snapshots. Changes made during a scan may appear on the next scan. This integration is exercised with local HTTP fixtures; deployment still needs real provider app configuration and qualification.