Getting Started
Core Architecture
Link Engine
Analytics & Attribution
Partners & Affiliates
Third-Party Integrations
Identity & Security
Automation & Messaging
Developer Tools
The following files were used as context for generating this wiki page:
Dub provides a robust OAuth2 provider and API token management system that secures programmatic access and enables third-party application integrations across workspaces and user accounts. The platform supports standard authorization code flows with PKCE and client secrets, granular permission scopes, token caching via Upstash Redis, and dedicated endpoints for identity resolution and token lifecycles.
Sources: apps/web/app/api/oauth/authorize/route.ts:1-134, apps/web/app/api/oauth/token/route.ts:1-30, apps/web/lib/auth/token-cache.ts:1-76, apps/web/app/api/oauth/userinfo/route.ts:1-84
OAuth applications are modeled through a relational Prisma schema combining general integration metadata with OAuth-specific credentials. Each registered application is backed by an Integration record linked to a project workspace and user, containing descriptive properties such as names, slugs, developers, websites, install URLs, descriptions, readmes, logos, and screenshots. The associated OAuthApp record stores unique client identifiers, hashed client secrets, partial secret previews, redirect URIs in JSON format, and PKCE requirement flags.
The OAuth application lifecycle is managed via REST endpoints under /api/oauth/apps and /api/oauth/apps/[appId], which enforce workspace permissions (oauth_apps.read and oauth_apps.write). When creating an application via POST /api/oauth/apps, the request body is parsed and validated against createOAuthAppSchema. The execution sequence proceeds as follows: parseRequestBody() → createOAuthAppSchema.parseAsync() → prisma.integration.findUnique() checking for slug conflicts → createToken() generating clientId and optionally clientSecret → prisma.integration.create() creating the integration and nested oAuthApp records → conditional storage upload for application logos via storage.upload().
Warning
If a slug conflict occurs during application creation or updating, Prisma throws error code P2002, which is caught and re-thrown as a DubApiError with a conflict status code and a message indicating the slug is already in use.
Sources: apps/web/app/api/oauth/apps/route.ts:67-72, apps/web/app/api/oauth/apps/route.ts:143-155, apps/web/app/api/oauth/apps/appId/route.ts:153-165
Updates and deletions handled by PATCH /api/oauth/apps/[appId] and DELETE /api/oauth/apps/[appId] utilize Vercel's waitUntil function to asynchronously clean up stale logo assets and removed screenshot URLs from object storage without blocking the HTTP response.
Client identifiers and secrets adhere to strict length and prefix conventions defined in the OAuth configuration constants. Client IDs use the prefix dub_app_ with a length of 24 characters, while client secrets use the prefix dub_app_secret_ with a length of 30 characters. When applications do not enforce PKCE (pkce: false), a client secret is generated upon creation.
Developers can also regenerate client secrets out-of-band using the server action generateClientSecret. This action validates workspace permissions (oauth_apps.write), confirms ownership of the integration via prisma.integration.findFirstOrThrow(), generates a new token using createToken(), updates the database record with a newly hashed secret via hashToken(), and stores a masked partial secret preview displaying the last 8 characters (dub_app_secret_****${clientSecret.slice(-8)}).
The authorization code flow governs how third-party applications request delegated access to user workspaces. The consent UI page (apps/web/app/app.dub.co/(auth)/oauth/authorize/page.tsx) validates incoming query parameters against the authorizeRequestSchema using validateAuthorizeRequest(). If valid, it presents a consent screen displaying the requesting application's name, logo, developer, and requested permission scopes, along with an optional verification warning banner for unverified apps.
Approval of an authorization request is handled by the POST /api/oauth/authorize endpoint. The execution sequence proceeds as follows: authorizeRequestSchema.parse() → getGrantedScopesForRole() filtering requested scopes against the workspace role → prisma.oAuthApp.findUniqueOrThrow() fetching application metadata and integration status → plan check for restricted integrations → redirect URI validation against app whitelist → conditional PKCE presence check → canInstallOAuthApp() verification → prisma.oAuthCode.create() issuing an authorization code.
Warning
If a request specifies Stripe or Shopify integration IDs (STRIPE_INTEGRATION_ID, SHOPIFY_INTEGRATION_ID) while the workspace is on a free or pro plan, the authorization request fails immediately with a bad_request error requiring a Business plan upgrade.
The OAuth provider defines twelve distinct permission scopes that applications can request. Each scope governs specific read or write capabilities across workspace resources.
Note
The user.read scope is granted by default to all authorization requests without requiring applications to explicitly request it in the authorization URL.
The token exchange and refresh lifecycle is handled through POST /api/oauth/token, which parses incoming form data against tokenGrantSchema and branches based on the requested grant_type. The route supports two primary grant types: authorization_code and refresh_token, each enforcing strict client authentication, expiration checks, and token rotation rules.
When a client presents an authorization code, exchangeAuthCodeForToken validates the request parameters, authenticates the client via HTTP Basic Auth or direct body parameters when PKCE is disabled, and verifies code integrity.
The execution sequence proceeds as follows: tokenGrantSchema.parse() → exchangeAuthCodeForToken() → Promise.all([prisma.oAuthApp.findUnique(), prisma.oAuthCode.findUnique()]) fetching application configuration and code record → PKCE or client secret verification → code expiration and redirect URI comparison → installIntegration() provisioning workspace access → prisma.restrictedToken.create() persisting the access token and nested refresh token → waitUntil() executing deferred cleanup (prisma.oAuthCode.delete() and prisma.restrictedToken.deleteMany()).
Caution
If PKCE is enabled on the OAuthApp, a code_verifier parameter is mandatory. When the code challenge method is S256, the verifier is hashed via generateCodeChallengeHash() and compared against accessCode.codeChallenge; a mismatch throws an unauthorized error with the invalid_grant code.
The refreshAccessToken function processes refresh_token grants by validating client credentials, looking up the hashed refresh token, checking expiration, and issuing a rotated token pair.
The execution sequence proceeds as follows: tokenGrantSchema.parse() → refreshAccessToken() → Basic Auth header parsing if client credentials are omitted from the body → prisma.oAuthApp.findUnique() checking app and PKCE settings → prisma.oAuthRefreshToken.findUnique() querying the hashed refresh token → prisma.installedIntegration.findUnique() loading installation and project plan metadata → prisma.$transaction() deleting the old access token (prisma.restrictedToken.delete()) and creating the new token pair (prisma.restrictedToken.create()) with a fresh refresh token record.
Important
Token lifetimes and key generation parameters are centrally defined in OAUTH_CONFIG. Access tokens have a lifetime of 2 hours (7200 seconds) and a length of 40 characters with a dub_access_token_ prefix, while refresh tokens have a lifetime of 120 days and a 40-character length without a prefix. Authorization codes expire after 2 minutes.
The OAuth token storage schema coordinates relational ownership between applications, authorization codes, restricted tokens, and refresh tokens across database tables.
The UserInfo endpoint (GET /api/oauth/userinfo) resolves the authenticated user and associated workspace identity from a bearer token. It implements CORS support via predefined headers (Access-Control-Allow-Origin: *, Access-Control-Allow-Methods: GET, OPTIONS, and Access-Control-Allow-Headers: Content-Type, Authorization) and handles preflight OPTIONS requests by returning status 204.
Sources: apps/web/app/api/oauth/userinfo/route.ts:7-14, apps/web/app/api/oauth/userinfo/route.ts:78-83
The execution sequence proceeds as follows: GET() → getAuthTokenOrThrow(req) extracts the bearer token from the request → hashToken(accessToken) hashes the token string → prisma.restrictedToken.findFirst() queries the database for an active, non-expired token matching the hash where installationId is not null → selection extracts the related user (id, name, image) and project (id, name, slug, logo) records → prefixWorkspaceId(tokenRecord.project.id) formats the workspace identifier → NextResponse.json(userInfo) serializes the payload.
Warning
If tokenRecord is not found or has expired (expires < new Date()), or lacks an installationId, the lookup fails and throws a DubApiError with code unauthorized and message "Access token not found or expired.".
Dub supports scoped personal and workspace API tokens used by external applications and automation clients to interact with workspace resources. Tokens are managed through standard REST API endpoints supporting retrieval, creation, updating, and revocation, alongside dashboard client components. Workspace tokens enforce limits, role validation, and optional machine-user provisioning.
The API provides structured operations for inspecting, modifying, and deleting tokens within a workspace context. Each route enforces required permissions via workspace authorization middleware.
Sources: apps/web/app/api/tokens/route.ts:26-65, apps/web/app/api/tokens/route.ts:68-184, apps/web/app/api/tokens/id/route.ts:12-150
Warning
Workspace token creation is capped at a maximum of 100 active tokens (MAX_WORKSPACE_TOKENS). Exceeding this limit throws a forbidden DubApiError prompting users to contact support.
The token creation workflow (POST /api/tokens) executes the following sequential stages: POST() → assertRateLimit() checks creation frequency against RATELIMIT_POLICIES.createToken → createTokenSchema.parse() validates request body parameters (name, isMachine, scopes) → workspace user role validation confirms authorization → hashToken() generates a secure hash of the raw dub_{nanoid(24)} token → prisma transaction counts existing tokens against MAX_WORKSPACE_TOKENS (using isolation level ReadUncommitted) → if isMachine is true, a dedicated machine user is created with a user_ prefix and associated as a workspace member → prisma.restrictedToken.create() inserts the token record with hashed key, partial display key, and space-separated scopes → waitUntil() dispatches an email notification via sendEmail() with the APIKeyCreated template → NextResponse.json() returns the plain text token once.
Note
Only workspace owners can create machine users (isMachine: true). Attempting to create a machine user with a non-owner role throws a forbidden DubApiError.
Workspace authentication middleware authenticates incoming requests by inspecting Authorization Bearer tokens, checking Upstash Redis cache via hashed keys, querying Prisma for restricted or legacy tokens, validating expiration, enforcing plan-based rate limits, and updating token last-used timestamps asynchronously.
The authentication flow executes the following call chain: withWorkspace() wrapper executes → clones incoming request via req.clone() → extracts Authorization header → checks Bearer prefix via authorizationHeader.startsWith("Bearer ") → extracts raw API key → computes hashedKey = await hashToken(apiKey) → queries Redis cache via tokenCache.get({ hashedKey }) → if cache misses, queries database via prisma.restrictedToken.findUnique() for restricted tokens (dub_ prefix) or prisma.token.findUnique() for legacy tokens → validates token existence and token.user presence → checks token.expires < new Date() for expiration → if cache missed, persists item via background waitUntil(tokenCache.set({ hashedKey, token })) → evaluates plan rate limits via rateLimitRequest() using identifier workspace:ratelimit:${hashedKey} → updates token lastUsed timestamp asynchronously via ratelimit(1, "1 m").limit() and database update → populates session with user metadata.
Warning
Requests lacking the Bearer prefix on their Authorization header immediately throw a bad_request DubApiError requiring the correct schema prefix.
The TokenCache class interfaces with Upstash Redis (redis) using the prefix dubTokenCache and a default 24-hour expiration (CACHE_EXPIRATION). Cached items adhere to a Zod schema validating user details, scopes, and project plans.
Note
When a token is updated via PATCH /api/tokens/:id or deleted via DELETE /api/tokens/:id, the token cache is explicitly synchronized using waitUntil(tokenCache.set(...)) or waitUntil(tokenCache.delete(...)).
Scope validation ensures that users and tokens possess appropriate permissions before executing workspace operations. Role-based scope checks are enforced during token updates and workspace route handlers.
Caution
The PATCH /api/tokens/:id route validates requested scopes against the user's project role via validateScopesForRole(scopes, role). If any requested scope is unavailable for that role, an unprocessable_entity DubApiError is thrown.
First-party client integrations and third-party service connections within Dub rely on specialized OAuth provider abstractions and client helper utilities. The architecture standardizes OAuth interactions across CLI commands, Stripe App extensions, and webhook integrations like Bitly, Slack, Intercom, HubSpot, and Google Ads.
Sources: packages/stripe-app/src/utils/oauth.ts:1-150, packages/cli/src/utils/oauth.ts:1-9, apps/web/lib/integrations/oauth-provider.ts:1-174
The OAuthProvider class handles authorization URL generation, state storage in Upstash Redis with a 30-minute expiration, and code exchange using configurable body formats and authorization methods.
Note
Concrete implementations extend OAuthProvider to customize parameters, such as googleAdsOAuthProvider, which configures offline access and custom scopes.
Sources: apps/web/lib/integrations/google-ads/oauth.ts:11-38, apps/web/lib/integrations/google-ads/oauth.ts:222-233
The CLI authentication lifecycle initiates via the login command, which generates a PKCE code verifier (getNanoid(64)), constructs the authorization URI using @badgateway/oauth2-client, opens the browser via open(authUrl), and spawns a local HTTP server on port 4587 to capture the callback.
The call chain for the CLI callback handling executes: oauthCallbackServer() creates an HTTP server listening on port 4587 (server.listen(4587)) with a 5-minute timeout (setTimeout(..., 300000)) → incoming requests are parsed via url.parse(req.url || "", true) → verifies reqUrl.pathname === "/callback" and req.method === "GET" → extracts the authorization code query parameter → invokes oauthClient.authorizationCode.getToken({ code, redirectUri, codeVerifier }) to exchange credentials → invokes setConfig(configInfo) to persist tokens and domain (dub.sh) → terminates the server via server.close() and exits the process via process.exit(0).
Warning
If the callback server does not receive an authorization code within 300,000 milliseconds (5 minutes), the timeout handler automatically closes the server and terminates the process.
The Stripe App integration manages Dub authentication inside Stripe dashboards using mode-specific redirect URLs and secure secret storage.
export async function getValidToken({ stripe }: { stripe: Stripe }) {
const token = await getSecret<Token>({
stripe,
name: "dub_token",
});
if (!token) {
throw new Error("Access token not found for the account.");
}
try {
await getUserInfo({ token });
} catch (e) {
const refreshedToken = await refreshToken({ token });
if (!refreshedToken) {
console.error("Failed to refresh access token.");
return null;
}
await setSecret({
stripe,
name: "dub_token",
payload: JSON.stringify(refreshedToken),
});
return refreshedToken;
}
return token;
}