|
| 1 | +--- |
| 2 | +title: 'Management API SDK' |
| 3 | +metaTitle: 'Prisma Postgres: Management API SDK' |
| 4 | +metaDescription: 'A TypeScript SDK for the Prisma Data Platform Management API with built-in OAuth authentication and automatic token refresh.' |
| 5 | +--- |
| 6 | + |
| 7 | +## Overview |
| 8 | + |
| 9 | +The [`@prisma/management-api-sdk`](https://www.npmjs.com/package/@prisma/management-api-sdk) is a TypeScript SDK for the [Prisma Data Platform Management API](/postgres/introduction/management-api) with built-in OAuth authentication and automatic token refresh. |
| 10 | + |
| 11 | +## Installation |
| 12 | + |
| 13 | +```terminal |
| 14 | +npm install @prisma/management-api-sdk |
| 15 | +``` |
| 16 | + |
| 17 | +## Quick start |
| 18 | + |
| 19 | +```typescript |
| 20 | +import { createManagementAPI, type TokenStorage } from '@prisma/management-api-sdk' |
| 21 | + |
| 22 | +// Implement token storage for your environment |
| 23 | +const tokenStorage: TokenStorage = { |
| 24 | + async getTokens() { |
| 25 | + const stored = localStorage.getItem('prisma-tokens') |
| 26 | + return stored ? JSON.parse(stored) : null |
| 27 | + }, |
| 28 | + async setTokens(tokens) { |
| 29 | + localStorage.setItem('prisma-tokens', JSON.stringify(tokens)) |
| 30 | + }, |
| 31 | + async clearTokens() { |
| 32 | + localStorage.removeItem('prisma-tokens') |
| 33 | + }, |
| 34 | +} |
| 35 | + |
| 36 | +// Create the API instance |
| 37 | +const api = createManagementAPI({ |
| 38 | + clientId: 'your-oauth-client-id', |
| 39 | + redirectUri: 'https://your-app.com/auth/callback', |
| 40 | + tokenStorage, |
| 41 | +}) |
| 42 | + |
| 43 | +// Use the typed API client |
| 44 | +const { data, error } = await api.client.GET('/v1/workspaces') |
| 45 | +``` |
| 46 | + |
| 47 | +## Authentication flow |
| 48 | + |
| 49 | +The SDK uses OAuth 2.0 with PKCE for secure authentication. The flow is stateless - you're responsible for storing the state and verifier between the login URL generation and callback handling. |
| 50 | + |
| 51 | +### 1. Initiate login |
| 52 | + |
| 53 | +Generate the OAuth login URL. The returned `state` and `verifier` must be stored (e.g., in a session or cookie) for use when handling the callback: |
| 54 | + |
| 55 | +```typescript |
| 56 | +const { url, state, verifier } = await api.getLoginUrl({ |
| 57 | + scope: 'workspace:admin offline_access', |
| 58 | + additionalParams: { |
| 59 | + utm_source: 'my-app', |
| 60 | + utm_medium: 'login', |
| 61 | + }, |
| 62 | +}) |
| 63 | + |
| 64 | +// Store state and verifier for the callback (e.g., in session storage) |
| 65 | +sessionStorage.setItem('oauth-state', state) |
| 66 | +sessionStorage.setItem('oauth-verifier', verifier) |
| 67 | + |
| 68 | +// Redirect user to the login URL |
| 69 | +window.location.href = url |
| 70 | +``` |
| 71 | + |
| 72 | +### 2. Handle the callback |
| 73 | + |
| 74 | +When the user is redirected back to your app, retrieve the stored state and verifier and pass them to `handleCallback`. On success, tokens are automatically stored via your `tokenStorage` implementation: |
| 75 | + |
| 76 | +```typescript |
| 77 | +// In your callback route handler |
| 78 | +const callbackUrl = window.location.href |
| 79 | + |
| 80 | +// Retrieve the stored values |
| 81 | +const expectedState = sessionStorage.getItem('oauth-state') |
| 82 | +const verifier = sessionStorage.getItem('oauth-verifier') |
| 83 | + |
| 84 | +// Clean up stored values |
| 85 | +sessionStorage.removeItem('oauth-state') |
| 86 | +sessionStorage.removeItem('oauth-verifier') |
| 87 | + |
| 88 | +try { |
| 89 | + await api.handleCallback({ |
| 90 | + callbackUrl, |
| 91 | + verifier, |
| 92 | + expectedState, |
| 93 | + }) |
| 94 | + |
| 95 | + // Tokens are now stored in tokenStorage and the client is ready to use |
| 96 | + console.log('Login successful!') |
| 97 | +} catch (error) { |
| 98 | + if (error instanceof AuthError) { |
| 99 | + console.error('Authentication failed:', error.message) |
| 100 | + } |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +### 3. Make API calls |
| 105 | + |
| 106 | +The client automatically includes authentication headers and refreshes tokens when they expire: |
| 107 | + |
| 108 | +```typescript |
| 109 | +// List workspaces |
| 110 | +const { data: workspaces } = await api.client.GET('/v1/workspaces') |
| 111 | + |
| 112 | +// Get a specific project |
| 113 | +const { data: project } = await api.client.GET('/v1/projects/{id}', { |
| 114 | + params: { path: { id: 'project-id' } }, |
| 115 | +}) |
| 116 | + |
| 117 | +// Create a new project |
| 118 | +const { data: newProject } = await api.client.POST('/v1/workspaces/{workspaceId}/projects', { |
| 119 | + params: { path: { workspaceId: 'workspace-id' } }, |
| 120 | + body: { name: 'My Project' }, |
| 121 | +}) |
| 122 | +``` |
| 123 | + |
| 124 | +### 4. Logout |
| 125 | + |
| 126 | +```typescript |
| 127 | +await api.logout() // Clears stored tokens |
| 128 | +``` |
| 129 | + |
| 130 | +## API reference |
| 131 | + |
| 132 | +### `createManagementAPI(config)` |
| 133 | + |
| 134 | +Creates a Management API instance with authentication handling. |
| 135 | + |
| 136 | +```typescript |
| 137 | +import { createManagementAPI } from '@prisma/management-api-sdk' |
| 138 | + |
| 139 | +const api = createManagementAPI({ |
| 140 | + clientId: 'your-oauth-client-id', |
| 141 | + redirectUri: 'https://your-app.com/auth/callback', |
| 142 | + tokenStorage, |
| 143 | + apiBaseUrl: 'https://api.prisma.io', // optional |
| 144 | + authBaseUrl: 'https://auth.prisma.io', // optional |
| 145 | +}) |
| 146 | +``` |
| 147 | + |
| 148 | +Returns an object with: |
| 149 | + |
| 150 | +| Property | Description | |
| 151 | +|----------|-------------| |
| 152 | +| `client` | The typed API client for making requests | |
| 153 | +| `getLoginUrl(options)` | Generate OAuth login URL with specified scope | |
| 154 | +| `handleCallback(options)` | Handle OAuth callback and store tokens via tokenStorage | |
| 155 | +| `logout()` | Clear stored tokens | |
| 156 | + |
| 157 | +### `createManagementAPIClient(options)` |
| 158 | + |
| 159 | +Creates a raw API client without authentication handling. Useful if you want to manage authentication yourself. |
| 160 | + |
| 161 | +```typescript |
| 162 | +import { createManagementAPIClient } from '@prisma/management-api-sdk' |
| 163 | + |
| 164 | +const client = createManagementAPIClient({ |
| 165 | + baseUrl: 'https://api.prisma.io', |
| 166 | + headers: { |
| 167 | + Authorization: `Bearer ${myToken}`, |
| 168 | + }, |
| 169 | +}) |
| 170 | + |
| 171 | +const { data } = await client.GET('/v1/workspaces') |
| 172 | +``` |
| 173 | + |
| 174 | +### Configuration options |
| 175 | + |
| 176 | +```typescript |
| 177 | +type ManagementAPIClientConfig = { |
| 178 | + // Required |
| 179 | + clientId: string // OAuth client ID |
| 180 | + redirectUri: string // OAuth redirect URI |
| 181 | + tokenStorage: TokenStorage |
| 182 | + |
| 183 | + // Optional (with defaults) |
| 184 | + apiBaseUrl?: string // Default: 'https://api.prisma.io' |
| 185 | + authBaseUrl?: string // Default: 'https://auth.prisma.io' |
| 186 | +} |
| 187 | +``` |
| 188 | +
|
| 189 | +### Token storage interface |
| 190 | +
|
| 191 | +Implement this interface to handle token persistence in your environment: |
| 192 | +
|
| 193 | +```typescript |
| 194 | +interface TokenStorage { |
| 195 | + /** Provide the stored tokens to the SDK */ |
| 196 | + getTokens(): Promise<Tokens | null> |
| 197 | + /** Store new or updated tokens when the SDK has successfully authenticated or refreshed tokens */ |
| 198 | + setTokens(tokens: Tokens): Promise<void> |
| 199 | + /** Clear the tokens when the user logs out or the refresh token is invalid */ |
| 200 | + clearTokens(): Promise<void> |
| 201 | +} |
| 202 | + |
| 203 | +type Tokens = { |
| 204 | + /** The workspace ID that these tokens are valid for (extracted from the access token) */ |
| 205 | + workspaceId: string |
| 206 | + /** The access token for API requests */ |
| 207 | + accessToken: string |
| 208 | + /** The refresh token for obtaining new access tokens (only present if scope includes 'offline_access') */ |
| 209 | + refreshToken?: string |
| 210 | +} |
| 211 | +``` |
| 212 | +
|
| 213 | +## Examples |
| 214 | +
|
| 215 | +### VS Code extension |
| 216 | +
|
| 217 | +```typescript |
| 218 | +const tokenStorage: TokenStorage = { |
| 219 | + async getTokens() { |
| 220 | + const workspaceId = await context.secrets.get('workspaceId') |
| 221 | + const accessToken = await context.secrets.get('accessToken') |
| 222 | + const refreshToken = await context.secrets.get('refreshToken') |
| 223 | + |
| 224 | + if (!workspaceId || !accessToken) return null |
| 225 | + |
| 226 | + return { workspaceId, accessToken, refreshToken: refreshToken || undefined } |
| 227 | + }, |
| 228 | + async setTokens(tokens) { |
| 229 | + await context.secrets.store('workspaceId', tokens.workspaceId) |
| 230 | + await context.secrets.store('accessToken', tokens.accessToken) |
| 231 | + if (tokens.refreshToken) { |
| 232 | + await context.secrets.store('refreshToken', tokens.refreshToken) |
| 233 | + } |
| 234 | + }, |
| 235 | + async clearTokens() { |
| 236 | + await context.secrets.delete('workspaceId') |
| 237 | + await context.secrets.delete('accessToken') |
| 238 | + await context.secrets.delete('refreshToken') |
| 239 | + }, |
| 240 | +} |
| 241 | +``` |
| 242 | + |
| 243 | +### Node.js CLI |
| 244 | + |
| 245 | +```typescript |
| 246 | +import { readFile, writeFile, unlink } from 'node:fs/promises' |
| 247 | +import { homedir } from 'node:os' |
| 248 | +import { join } from 'node:path' |
| 249 | + |
| 250 | +const tokenPath = join(homedir(), '.prisma', 'credentials.json') |
| 251 | + |
| 252 | +const tokenStorage: TokenStorage = { |
| 253 | + async getTokens() { |
| 254 | + try { |
| 255 | + const data = await readFile(tokenPath, 'utf-8') |
| 256 | + return JSON.parse(data) |
| 257 | + } catch { |
| 258 | + return null |
| 259 | + } |
| 260 | + }, |
| 261 | + async setTokens(tokens) { |
| 262 | + await writeFile(tokenPath, JSON.stringify(tokens, null, 2)) |
| 263 | + }, |
| 264 | + async clearTokens() { |
| 265 | + await unlink(tokenPath).catch(() => {}) |
| 266 | + }, |
| 267 | +} |
| 268 | +``` |
| 269 | + |
| 270 | +### Stateless web server |
| 271 | + |
| 272 | +For stateless web servers (serverless, load-balanced), store the PKCE state in an encrypted cookie or database: |
| 273 | + |
| 274 | +```typescript |
| 275 | +// In your login route |
| 276 | +app.get('/login', async (req, res) => { |
| 277 | + const { url, state, verifier } = await api.getLoginUrl({ |
| 278 | + scope: 'workspace:admin offline_access', |
| 279 | + }) |
| 280 | + |
| 281 | + // Store in encrypted cookie or database keyed by state |
| 282 | + res.cookie('oauth-verifier', verifier, { httpOnly: true, secure: true, signed: true }) |
| 283 | + res.cookie('oauth-state', state, { httpOnly: true, secure: true, signed: true }) |
| 284 | + |
| 285 | + res.redirect(url) |
| 286 | +}) |
| 287 | + |
| 288 | +// In your callback route |
| 289 | +app.get('/callback', async (req, res) => { |
| 290 | + const verifier = req.signedCookies['oauth-verifier'] |
| 291 | + const expectedState = req.signedCookies['oauth-state'] |
| 292 | + |
| 293 | + // Clear cookies |
| 294 | + res.clearCookie('oauth-verifier') |
| 295 | + res.clearCookie('oauth-state') |
| 296 | + |
| 297 | + await api.handleCallback({ callbackUrl: req.url, verifier, expectedState }) |
| 298 | + |
| 299 | + // Tokens are now stored in tokenStorage |
| 300 | + // ... handle successful login |
| 301 | +}) |
| 302 | +``` |
| 303 | + |
| 304 | +## TypeScript types |
| 305 | + |
| 306 | +The SDK exports all API types generated from the OpenAPI spec: |
| 307 | + |
| 308 | +```typescript |
| 309 | +import type { paths, components } from '@prisma/management-api-sdk' |
| 310 | + |
| 311 | +// Access response types |
| 312 | +type Workspace = components['schemas']['Workspace'] |
| 313 | +type Project = components['schemas']['Project'] |
| 314 | +``` |
| 315 | +
|
| 316 | +## Error handling |
| 317 | +
|
| 318 | +The SDK exports two error classes: |
| 319 | +
|
| 320 | +### `AuthError` |
| 321 | +
|
| 322 | +Thrown for authentication-related errors: |
| 323 | +
|
| 324 | +- OAuth callback errors (includes `error_description` when available) |
| 325 | +- Invalid or missing tokens |
| 326 | +- Token refresh failures |
| 327 | +
|
| 328 | +```typescript |
| 329 | +import { AuthError } from '@prisma/management-api-sdk' |
| 330 | + |
| 331 | +try { |
| 332 | + await api.handleCallback({ callbackUrl, verifier, expectedState }) |
| 333 | +} catch (error) { |
| 334 | + if (error instanceof AuthError) { |
| 335 | + if (error.refreshTokenInvalid) { |
| 336 | + // Token is invalid/expired, user needs to log in again |
| 337 | + const { url } = await api.getLoginUrl({ scope: 'workspace:admin offline_access' }) |
| 338 | + // redirect to url... |
| 339 | + } else { |
| 340 | + // Other auth errors (e.g., "access_denied: User cancelled") |
| 341 | + console.error('Auth error:', error.message) |
| 342 | + } |
| 343 | + } |
| 344 | +} |
| 345 | +``` |
| 346 | + |
| 347 | +### `FetchError` |
| 348 | + |
| 349 | +Thrown for network-related errors. Includes the original error as `cause` for debugging: |
| 350 | + |
| 351 | +```typescript |
| 352 | +import { FetchError } from '@prisma/management-api-sdk' |
| 353 | + |
| 354 | +try { |
| 355 | + const { data } = await api.client.GET('/v1/workspaces') |
| 356 | +} catch (error) { |
| 357 | + if (error instanceof FetchError) { |
| 358 | + console.error('Network error:', error.message) |
| 359 | + console.error('Cause:', error.cause) // Original error for debugging |
| 360 | + } |
| 361 | +} |
| 362 | +``` |
| 363 | + |
| 364 | +## Automatic token refresh |
| 365 | + |
| 366 | +The SDK automatically handles token refresh when a refresh token is available (requires `offline_access` scope): |
| 367 | + |
| 368 | +- When a request returns `401`, the SDK refreshes the access token using the refresh token |
| 369 | +- Concurrent requests during refresh are queued and resolved once refresh completes |
| 370 | +- If refresh fails due to an invalid refresh token, tokens are cleared and `AuthError` is thrown with `refreshTokenInvalid: true` |
| 371 | +- If no refresh token is available, an `AuthError` is thrown with the message `"No refresh token available. Please log in again."` |
0 commit comments