# @prisma/compute-sdk

TypeScript SDK for deploying and managing applications on [Prisma Compute](https://www.prisma.io/blog/prisma-compute).

## Installation

```bash
npm install @prisma/compute-sdk @prisma/management-api-sdk
```

`@prisma/management-api-sdk` is a peer dependency that provides the authenticated API client.

## Prerequisites

You need an authenticated `ManagementApiClient` from `@prisma/management-api-sdk`. There are two ways to create one:

### Using a service token

```ts
import { createManagementApiClient } from "@prisma/management-api-sdk";

const apiClient = createManagementApiClient({
  token: process.env.PRISMA_API_TOKEN,
});
```

### Using OAuth

```ts
import { createManagementApiSdk } from "@prisma/management-api-sdk";

const sdk = createManagementApiSdk({
  clientId: "your-client-id",
  redirectUri: "http://localhost:3000/callback",
  tokenStorage: yourTokenStorageImpl, // implements TokenStorage interface
});

// sdk.client is a ManagementApiClient with automatic token refresh
const apiClient = sdk.client;
```

See the [`@prisma/management-api-sdk` documentation](https://www.npmjs.com/package/@prisma/management-api-sdk) for full details on authentication setup.

## Quick start

```ts
import { ComputeClient, PreBuilt, Ok } from "@prisma/compute-sdk";
import { createManagementApiClient } from "@prisma/management-api-sdk";

const apiClient = createManagementApiClient({
  token: process.env.PRISMA_API_TOKEN,
});

const compute = new ComputeClient(apiClient);

// Deploy a pre-built application
const result = await compute.deploy({
  strategy: new PreBuilt({
    appPath: "./dist",
    entrypoint: "index.js",
  }),
  projectId: "your-project-id",
  appName: "my-app",
  region: "us-east-1",
});

if (result.isOk()) {
  console.log(`Deployed to ${result.value.deploymentEndpointDomain}`);
} else {
  console.error(`Deploy failed: ${result.error.message}`);
}
```

## API reference

### `ComputeClient`

The main entry point for all operations. Created from a `ManagementApiClient`:

```ts
import { ComputeClient } from "@prisma/compute-sdk";

const compute = new ComputeClient(apiClient);
```

All methods return `Promise<Result<T, E>>` — a discriminated union that is either `Ok` with a value or `Err` with a typed error. Each method declares only the error variants it can actually produce. See [Error handling](#error-handling) for details.

---

#### `deploy(options): Promise<Result<DeployResult, DeployError>>`

Builds, uploads, and deploys an application, then promotes it to the app's live endpoint.

```ts
const result = await compute.deploy({
  // Required: how to produce the deployable artifact
  strategy: new PreBuilt({ appPath: "./dist", entrypoint: "index.js" }),

  // Target (provide appId OR projectId + appName + region)
  projectId: "proj_abc",
  appName: "my-app",
  region: "us-east-1",
  // OR:
  appId: "app_xyz",

  // Optional
  // Stored through the environment-variable API before the deployment is created.
  // Production apps use project vars; preview-branch apps use branch overrides.
  envVars: { DATABASE_URL: "postgresql://..." },
  portMapping: { http: 3000 },
  timeoutSeconds: 120, // max time to wait for "running" status
  pollIntervalMs: 1000, // how often to check status
  skipPromote: false, // deploy without promoting to the app's live endpoint
  destroyOldDeployment: false, // destroy (rather than just stop) the previously promoted deployment
  signal: abortController.signal,
  progress: {
    /* DeployProgress callbacks */
  },
  interaction: {
    /* DeployInteraction callbacks */
  },
});

if (result.isOk()) {
  const { deploymentEndpointDomain, deploymentId, appId, resolvedConfig } =
    result.value;
}
```

`skipPromote` and `destroyOldDeployment` are mutually exclusive — combining them returns an `InvalidOptionsError`, since there is no previously-active deployment to destroy when promotion is skipped.

**Returns** `DeployResult`:

| Field                      | Type                                                | Description                                             |
| -------------------------- | ---------------------------------------------------- | -------------------------------------------------------- |
| `projectId`                 | `string`                                              | Project ID                                                |
| `appId`                     | `string`                                              | App ID                                                    |
| `appName`                   | `string`                                              | App display name                                          |
| `region`                    | `string`                                              | Region identifier                                          |
| `deploymentId`               | `string`                                              | Created deployment ID                                      |
| `deploymentEndpointDomain`   | `string`                                              | Live URL of the new deployment                             |
| `appEndpointDomain`          | `string \| null`                                     | The app's live URL, or `null` when `skipPromote` was set   |
| `promoted`                   | `boolean`                                             | Whether the new deployment was promoted                    |
| `previousDeploymentId`       | `string \| null`                                     | The previously active deployment, if any                   |
| `previousDeploymentAction`   | `"stopped" \| "destroyed" \| "still-active" \| null`  | What happened to the previous deployment                   |
| `resolvedConfig`             | `ResolvedConfig`                                      | Final resolved configuration                                |

---

#### `updateEnv(options): Promise<Result<UpdateEnvResult, UpdateEnvError>>`

Updates project environment variables and/or port mapping, then creates a new deployment that reuses the code from the most recent deployment. Preview-branch apps update preview branch overrides.

```ts
const result = await compute.updateEnv({
  appId: "app_xyz",
  envVars: { DATABASE_URL: "postgresql://new-url..." },
  portMapping: { http: 8080 },
});
```

To remove an environment variable, set its value to `null`:

```ts
const result = await compute.updateEnv({
  appId: "app_xyz",
  envVars: {
    DATABASE_URL: "postgresql://new-url...", // set or update
    OLD_SECRET: null, // remove
  },
});
```

The app must have at least one existing deployment. If not, this returns a `NoExistingDeploymentError`.

---

#### `destroyDeployment(options): Promise<Result<DestroyDeploymentResult, DestroyDeploymentError>>`

Stops (if running) and deletes a single deployment.

```ts
const result = await compute.destroyDeployment({
  deploymentId: "dep_abc",
  // OR provide appId + interaction.selectDeployment for interactive selection
});
```

**Returns** `DestroyDeploymentResult`:

| Field            | Type      | Description                  |
| ---------------- | --------- | ----------------------------- |
| `deploymentId`   | `string`  | The destroyed deployment ID   |
| `previousStatus` | `string`  | Status before destruction     |
| `stopped`        | `boolean` | Whether a stop was required   |
| `deleted`        | `boolean` | Whether deletion succeeded    |

---

#### `destroyApp(options): Promise<Result<DestroyAppResult, DestroyAppError>>`

Stops all running deployments, deletes all deployments, and optionally deletes the app itself.

```ts
const result = await compute.destroyApp({
  appId: "app_xyz",
  keepApp: false, // set to true to keep the app record
});
```

If some deployments fail to stop or delete, returns a `DestroyAggregateError` with details on which succeeded and which failed.

---

#### `promote(options): Promise<Result<PromoteResult, PromoteError>>`

Promotes an existing deployment to the app's live endpoint, starting it first if it isn't already running. Unlike `deploy`, this does not build, upload, or create a new deployment — it operates on one that already exists.

```ts
const result = await compute.promote({
  appId: "app_xyz",
  deploymentId: "dep_abc",
  // OR provide interaction.selectDeployment for interactive selection
});
```

**Returns** `PromoteResult`:

| Field                | Type      | Description                                     |
| -------------------- | --------- | ------------------------------------------------ |
| `appId`              | `string`  | App ID                                            |
| `deploymentId`       | `string`  | The promoted deployment ID                        |
| `appEndpointDomain`  | `string`  | The app's live URL after promotion                |
| `deploymentStarted`  | `boolean` | Whether the deployment had to be started first    |

---

#### `createProject(options): Promise<Result<CreateProjectResult, ApiRequestError>>`

Creates a new project, optionally provisioning a database alongside it.

```ts
const result = await compute.createProject({
  name: "my-project",
  createDatabase: true,
  region: "us-east-1",
});

if (result.isOk()) {
  console.log(`Project: ${result.value.id}`);
  if (result.value.database) {
    console.log(`Database: ${result.value.database.connectionString}`);
  }
}
```

**Returns** `CreateProjectResult` (`ProjectInfo & { database?: DatabaseInfo }`):

| Field           | Type           | Description                                          |
| --------------- | -------------- | ------------------------------------------------------ |
| `id`             | `string`       | Project ID                                              |
| `name`           | `string`       | Project name                                             |
| `defaultRegion`  | `string \| undefined` | Default region, if set                            |
| `database`       | `DatabaseInfo \| undefined` | Provisioned database, present when `createDatabase` is true |

---

#### `listProjects(options?): Promise<Result<ProjectInfo[], ApiRequestError>>`

```ts
const result = await compute.listProjects();
if (result.isOk()) {
  for (const project of result.value) {
    console.log(`${project.name} (${project.id})`);
  }
}
```

---

#### `listApps(options): Promise<Result<AppInfo[], ApiRequestError>>`

```ts
const result = await compute.listApps({ projectId: "proj_abc" });
```

---

#### `createApp(options): Promise<Result<AppInfo, ApiRequestError>>`

```ts
const result = await compute.createApp({
  projectId: "proj_abc",
  appName: "my-new-app",
  region: "eu-west-3",
});
```

---

#### `showApp(options): Promise<Result<AppDetail, ApiRequestError>>`

```ts
const result = await compute.showApp({ appId: "app_xyz" });
if (result.isOk()) {
  console.log(`Latest deployment: ${result.value.latestDeploymentId}`);
}
```

---

#### `deleteApp(options): Promise<Result<void, ApiRequestError>>`

Deletes an app record. The app should have no deployments (use `destroyApp` to clean up deployments first).

```ts
await compute.deleteApp({ appId: "app_xyz" });
```

---

#### `listDeployments(options): Promise<Result<DeploymentInfo[], ApiRequestError>>`

```ts
const result = await compute.listDeployments({ appId: "app_xyz" });
```

---

#### `showDeployment(options): Promise<Result<DeploymentDetail, ApiRequestError>>`

```ts
const result = await compute.showDeployment({ deploymentId: "dep_abc" });
if (result.isOk()) {
  console.log(`Status: ${result.value.status}`);
  console.log(`URL: https://${result.value.previewDomain}`);
}
```

---

#### `startDeployment(options): Promise<Result<void, ApiRequestError>>`

```ts
await compute.startDeployment({ deploymentId: "dep_abc" });
```

---

#### `stopDeployment(options): Promise<Result<void, ApiRequestError>>`

```ts
await compute.stopDeployment({ deploymentId: "dep_abc" });
```

---

#### `deleteDeployment(options): Promise<Result<void, ApiRequestError>>`

```ts
await compute.deleteDeployment({ deploymentId: "dep_abc" });
```

## Repository snapshot detection

Use `detectComputeApp` when a consumer already has repository metadata but no
local checkout, such as a GitHub import flow. Detection is scoped to one app
root, so monorepo consumers enumerate workspace packages and call it once per
candidate.

```ts
import { detectComputeApp } from "@prisma/compute-sdk/config";

const detected = detectComputeApp({
  root: "apps/server",
  manifest: {
    main: "src/index.ts",
    dependencies: { elysia: "1.3.0" },
  },
  filePaths: [
    "apps/server/package.json",
    "apps/server/src/index.ts",
  ],
});
```

The result includes the Compute framework, build type, HTTP port, inferred
entrypoint, and the dependency, config file, or runtime script that provided
the detection evidence. It returns `null` when the package has no deployable
framework or runtime signal, so shared workspace libraries are not treated as
apps.

## Build strategies

A build strategy produces a deployable artifact (a directory with an entrypoint file). The SDK ships with two built-in general-purpose strategies (plus framework-specific ones used internally by `AutoBuild`):

### `PreBuilt`

Use when your application is already built (e.g., output of `tsc`, `esbuild`, or any other bundler):

```ts
import { PreBuilt } from "@prisma/compute-sdk";

const strategy = new PreBuilt({
  appPath: "./dist", // absolute or relative path to the build output
  entrypoint: "index.js", // relative to appPath
});
```

`PreBuilt` validates that the entrypoint exists and is a relative path that doesn't escape the application directory. It performs no copying or transformation.

### `BunBuild`

Use when you want the SDK to bundle your application using [Bun](https://bun.sh):

```ts
import { BunBuild } from "@prisma/compute-sdk";

const strategy = new BunBuild({
  appPath: "./my-app", // path to your application source
  entrypoint: "src/index.ts", // optional: resolved from package.json "main" if omitted
});
```

`BunBuild` runs `bun build` with `--target bun --sourcemap=external`, manages a temporary output directory, and cleans it up after the archive is created. Requires Bun to be installed on the machine.

### Custom strategies

Implement the `BuildStrategy` interface to use any build tool:

```ts
import type { BuildStrategy, BuildArtifact } from "@prisma/compute-sdk";

class MyCustomBuild implements BuildStrategy {
  async execute(): Promise<BuildArtifact> {
    // Run your build process...
    return {
      directory: "/path/to/output", // absolute path to the built files
      entrypoint: "index.js", // relative to directory, posix separators
      cleanup: async () => {
        // optional: called after archiving
        // clean up temp files
      },
    };
  }
}
```

## Error handling

All `ComputeClient` methods return `Result<T, E>` from the [`better-result`](https://www.npmjs.com/package/better-result) library instead of throwing exceptions. This gives you exhaustive, type-safe error handling.

### Checking results

```ts
const result = await compute.deploy({
  /* ... */
});

// Pattern 1: isOk / isErr
if (result.isOk()) {
  console.log(result.value.deploymentEndpointDomain);
} else {
  console.error(result.error.message);
}

// Pattern 2: match
result.match({
  Ok: (value) => console.log(value.deploymentEndpointDomain),
  Err: (error) => console.error(error.message),
});
```

### Error types

Every error extends `TaggedError` and has a `_tag` discriminant for pattern matching:

| Error class                | `_tag`                       | Description                                              |
| --------------------------- | ----------------------------- | ----------------------------------------------------------- |
| `AuthenticationError`        | `"AuthenticationError"`        | API returned HTTP 401                                        |
| `ApiError`                   | `"ApiError"`                   | API returned a non-401 error                                  |
| `MissingArgumentError`       | `"MissingArgumentError"`       | A required argument was not provided                           |
| `InvalidOptionsError`        | `"InvalidOptionsError"`        | A conflicting or invalid combination of options was provided    |
| `BuildError`                 | `"BuildError"`                 | Build strategy failed                                            |
| `ArtifactError`              | `"ArtifactError"`              | Archive creation or upload failed                                 |
| `TimeoutError`               | `"TimeoutError"`               | Deployment didn't reach target status in time                      |
| `DeploymentFailedError`      | `"DeploymentFailedError"`      | Deployment transitioned to `"failed"` status                        |
| `NoExistingDeploymentError`  | `"NoExistingDeploymentError"`  | `updateEnv` called on an app with no prior deployments                |
| `NoDeploymentsFoundError`    | `"NoDeploymentsFoundError"`    | App has no deployments to select from                                   |
| `CancelledError`             | `"CancelledError"`             | Operation cancelled via `AbortSignal`                                     |
| `DestroyAggregateError`      | `"DestroyAggregateError"`      | Some deployments failed during `destroyApp`                                |

### Matching specific errors

```ts
import { matchError, ApiError, AuthenticationError } from "@prisma/compute-sdk";

const result = await compute.deploy({
  /* ... */
});

if (result.isErr()) {
  matchError(result.error, {
    AuthenticationError: (e) => {
      console.error("Not authenticated. Check your token.");
    },
    ApiError: (e) => {
      console.error(`API error (${e.statusCode}): ${e.message}`);
      if (e.hint) console.error(`Hint: ${e.hint}`);
    },
    BuildError: (e) => {
      console.error(`Build failed: ${e.message}`);
    },
    TimeoutError: (e) => {
      console.error(`Timed out after ${Math.round(e.elapsedMs / 1000)}s`);
    },
    _: (e) => {
      console.error(`Unexpected error: ${e.message}`);
    },
  });
}
```

### Error type unions

The SDK exports narrowed error unions for each operation:

- **`DeployError`** — errors from `deploy()`: `CancelledError | MissingArgumentError | InvalidOptionsError | AuthenticationError | ApiError | BuildError | ArtifactError | TimeoutError | DeploymentFailedError`
- **`UpdateEnvError`** — errors from `updateEnv()`: `CancelledError | MissingArgumentError | InvalidOptionsError | AuthenticationError | ApiError | NoExistingDeploymentError | TimeoutError | DeploymentFailedError`
- **`DestroyDeploymentError`** — errors from `destroyDeployment()`: `CancelledError | MissingArgumentError | NoDeploymentsFoundError | AuthenticationError | ApiError | TimeoutError | DeploymentFailedError`
- **`DestroyAppError`** — errors from `destroyApp()`: `CancelledError | AuthenticationError | ApiError | DestroyAggregateError`
- **`PromoteError`** — errors from `promote()`: `AuthenticationError | ApiError | MissingArgumentError | NoDeploymentsFoundError | TimeoutError | DeploymentFailedError | CancelledError`
- **`ApiRequestError`** — errors from single-request methods (`createProject`, `listProjects`, `listApps`, `createApp`, `startDeployment`, `stopDeployment`, `deleteDeployment`, etc.): `CancelledError | AuthenticationError | ApiError`

## Progress and interaction callbacks

Long-running operations accept `progress` and `interaction` callbacks for UI integration.

### Deploy progress

`deploy()` promotes the new deployment and stops (optionally destroys) the previous one by default, so `DeployProgress` also reports promotion and old-deployment cleanup progress alongside the build/upload/start steps:

```ts
await compute.deploy({
  strategy,
  projectId: "proj_abc",
  appName: "my-app",
  region: "us-east-1",
  progress: {
    onBuildStart() {
      console.log("Building...");
    },
    onBuildComplete(artifact) {
      console.log(`Built to ${artifact.directory}`);
    },
    onArchiveCreating() {
      console.log("Creating archive...");
    },
    onArchiveReady(sizeBytes) {
      console.log(`Archive: ${(sizeBytes / 1024).toFixed(1)} KB`);
    },
    onDeploymentCreated(deploymentId) {
      console.log(`Deployment: ${deploymentId}`);
    },
    onUploadStart() {
      console.log("Uploading...");
    },
    onUploadComplete() {
      console.log("Upload complete.");
    },
    onStartRequested() {
      console.log("Starting...");
    },
    onStatusChange(status) {
      console.log(`Status: ${status}`);
    },
    onRunning(deploymentUrl) {
      console.log(`Live at ${deploymentUrl}`);
    },
    onPromoteStart() {
      console.log("Promoting...");
    },
    onPromoted(appEndpointDomain) {
      console.log(`Promoted: ${appEndpointDomain}`);
    },
    onOldDeploymentStopping(deploymentId) {
      console.log(`Stopping previous deployment ${deploymentId}...`);
    },
    onOldDeploymentStopped(deploymentId) {
      console.log(`Stopped previous deployment ${deploymentId}.`);
    },
  },
});
```

### Deploy interaction

When `projectId`, `appId`/`appName`, or `region` aren't provided, the SDK calls interaction callbacks so the caller can resolve them (e.g., by prompting the user):

```ts
await compute.deploy({
  strategy,
  interaction: {
    async selectProject(projects) {
      // projects: ProjectInfo[] — return the chosen project ID
      return projects[0].id;
    },
    async selectApp(apps) {
      // apps: AppInfo[] — return an app ID, or null to create a new one
      return null;
    },
    async provideAppName() {
      // return a name for the new app
      return "my-new-app";
    },
    async selectRegion(regions) {
      // regions: RegionInfo[] — used to resolve a region when one wasn't supplied
      return "us-east-1";
    },
  },
});
```

### Destroy progress

```ts
await compute.destroyApp({
  appId: "app_xyz",
  progress: {
    onStoppingDeployments(deploymentIds) {
      /* ... */
    },
    onDeploymentStopped(deploymentId) {
      /* ... */
    },
    onAllDeploymentsStopped() {
      /* ... */
    },
    onDeletingDeployments(deploymentIds) {
      /* ... */
    },
    onDeploymentDeleted(deploymentId) {
      /* ... */
    },
    onAllDeploymentsDeleted() {
      /* ... */
    },
    onAppDeleted(appId) {
      /* ... */
    },
  },
});
```

## Cancellation

All operations support cancellation via the standard `AbortSignal`:

```ts
const controller = new AbortController();

// Cancel after 30 seconds
setTimeout(() => controller.abort(), 30_000);

const result = await compute.deploy({
  strategy,
  appId: "app_xyz",
  signal: controller.signal,
});

if (result.isErr() && result.error._tag === "CancelledError") {
  console.log("Deployment was cancelled.");
}
```

## Domain types

```ts
import type {
  ProjectInfo, // { id, name, defaultRegion? }
  AppInfo, // { id, name, region, projectId, createdAt? }
  AppDetail, // AppInfo & { latestDeploymentId?, appEndpointDomain? }
  DeploymentInfo, // { id, status, createdAt, previewDomain? }
  DeploymentDetail, // DeploymentInfo & { envVars?, foundryVersionId? }
  RegionInfo, // { id, displayName }
  ResolvedConfig, // { projectId, appId, appName, region, portMapping? }
  PortMapping, // { http?: number | null }
} from "@prisma/compute-sdk";
```

### Available regions

```ts
import { REGIONS, KNOWN_REGION_IDS } from "@prisma/compute-sdk";

// KNOWN_REGION_IDS: readonly ["us-east-1", "us-west-1", "eu-west-3", "eu-central-1", "ap-northeast-1", "ap-southeast-1"]
// REGIONS: RegionInfo[] — same IDs with displayName
```

## Full example

```ts
import { ComputeClient, PreBuilt, matchError } from "@prisma/compute-sdk";
import { createManagementApiClient } from "@prisma/management-api-sdk";

async function main() {
  const apiClient = createManagementApiClient({
    token: process.env.PRISMA_API_TOKEN!,
  });

  const compute = new ComputeClient(apiClient);

  // Deploy
  const deployResult = await compute.deploy({
    strategy: new PreBuilt({
      appPath: "./dist",
      entrypoint: "server.js",
    }),
    projectId: process.env.PROJECT_ID!,
    appName: "my-api",
    region: "us-east-1",
    envVars: {
      DATABASE_URL: process.env.DATABASE_URL!,
      NODE_ENV: "production",
    },
    portMapping: { http: 3000 },
    progress: {
      onBuildStart: () => console.log("Preparing artifact..."),
      onUploadStart: () => console.log("Uploading..."),
      onStartRequested: () => console.log("Starting..."),
      onStatusChange: (s) => console.log(`  Status: ${s}`),
      onRunning: (url) => console.log(`Deployed: ${url}`),
    },
  });

  if (deployResult.isErr()) {
    matchError(deployResult.error, {
      AuthenticationError: () => {
        console.error("Invalid token. Set PRISMA_API_TOKEN.");
        process.exit(1);
      },
      BuildError: (e) => {
        console.error(`Build failed: ${e.message}`);
        process.exit(1);
      },
      TimeoutError: (e) => {
        console.error(`Deploy timed out (${Math.round(e.elapsedMs / 1000)}s).`);
        console.error(`Deployment ${e.deploymentId} may still be starting.`);
        process.exit(1);
      },
      _: (e) => {
        console.error(`Error: ${e.message}`);
        process.exit(1);
      },
    });
    return;
  }

  const { appId, deploymentId, deploymentEndpointDomain } = deployResult.value;
  console.log(`\nApp:        ${appId}`);
  console.log(`Deployment: ${deploymentId}`);
  console.log(`URL:        ${deploymentEndpointDomain}`);

  // List deployments
  const deploymentsResult = await compute.listDeployments({ appId });
  if (deploymentsResult.isOk()) {
    console.log(`\nDeployments (${deploymentsResult.value.length}):`);
    for (const d of deploymentsResult.value) {
      console.log(`  ${d.id} — ${d.status} (${d.createdAt})`);
    }
  }
}

main();
```

## Requirements

- Node.js >= 18.0.0
- `@prisma/management-api-sdk` ^1.44.0

## License

Apache-2.0
