# @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",
  serviceName: "my-app",
  region: "us-east-1",
});

if (result.isOk()) {
  console.log(`Deployed to ${result.value.deploymentUrl}`);
} 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 version.

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

  // Target (provide serviceId OR projectId + serviceName + region)
  projectId: "proj_abc",
  serviceName: "my-app",
  region: "us-east-1",
  // OR:
  serviceId: "svc_xyz",

  // Optional
  // Stored through the environment-variable API before the version is created.
  // Production services use project vars; preview-branch services 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
  signal: abortController.signal,
  progress: {
    /* DeployProgress callbacks */
  },
  interaction: {
    /* DeployInteraction callbacks */
  },
});

if (result.isOk()) {
  const { deploymentUrl, versionId, serviceId, resolvedConfig } = result.value;
}
```

**Returns** `DeployResult`:

| Field            | Type             | Description                  |
| ---------------- | ---------------- | ---------------------------- |
| `projectId`      | `string`         | Project ID                   |
| `serviceId`      | `string`         | Service ID                   |
| `serviceName`    | `string`         | Service display name         |
| `region`         | `string`         | Region identifier            |
| `versionId`      | `string`         | Created version ID           |
| `deploymentUrl`  | `string`         | Live URL of the deployment   |
| `resolvedConfig` | `ResolvedConfig` | Final resolved configuration |

---

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

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

```ts
const result = await compute.updateEnv({
  serviceId: "svc_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({
  serviceId: "svc_xyz",
  envVars: {
    DATABASE_URL: "postgresql://new-url...", // set or update
    OLD_SECRET: null, // remove
  },
});
```

The service must have at least one existing version. If not, this returns a `NoExistingVersionError`.

---

#### `destroyVersion(options): Promise<Result<DestroyVersionResult, DestroyVersionError>>`

Stops (if running) and deletes a single version.

```ts
const result = await compute.destroyVersion({
  versionId: "ver_abc",
  // OR provide serviceId + interaction.selectVersion for interactive selection
});
```

**Returns** `DestroyVersionResult`:

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

---

#### `destroyService(options): Promise<Result<DestroyServiceResult, DestroyServiceError>>`

Stops all running versions, deletes all versions, and optionally deletes the service itself.

```ts
const result = await compute.destroyService({
  serviceId: "svc_xyz",
  keepService: false, // set to true to keep the service record
});
```

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

---

#### `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})`);
  }
}
```

---

#### `listServices(options): Promise<Result<ServiceInfo[], ApiRequestError>>`

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

---

#### `createService(options): Promise<Result<ServiceInfo, ApiRequestError>>`

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

---

#### `showService(options): Promise<Result<ServiceDetail, ApiRequestError>>`

```ts
const result = await compute.showService({ serviceId: "svc_xyz" });
if (result.isOk()) {
  console.log(`Latest version: ${result.value.latestVersionId}`);
}
```

---

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

Deletes a service record. The service should have no versions (use `destroyService` to clean up versions first).

```ts
await compute.deleteService({ serviceId: "svc_xyz" });
```

---

#### `listVersions(options): Promise<Result<VersionInfo[], ApiRequestError>>`

```ts
const result = await compute.listVersions({ serviceId: "svc_xyz" });
```

---

#### `showVersion(options): Promise<Result<VersionDetail, ApiRequestError>>`

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

---

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

```ts
await compute.startVersion({ versionId: "ver_abc" });
```

---

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

```ts
await compute.stopVersion({ versionId: "ver_abc" });
```

---

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

```ts
await compute.deleteVersion({ versionId: "ver_abc" });
```

## Build strategies

A build strategy produces a deployable artifact (a directory with an entrypoint file). The SDK ships with two built-in strategies:

### `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.deploymentUrl);
} else {
  console.error(result.error.message);
}

// Pattern 2: match
result.match({
  Ok: (value) => console.log(value.deploymentUrl),
  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                      |
| `BuildError`             | `"BuildError"`             | Build strategy failed                                     |
| `ArtifactError`          | `"ArtifactError"`          | Archive creation or upload failed                         |
| `TimeoutError`           | `"TimeoutError"`           | Version didn't reach target status in time                |
| `VersionFailedError`     | `"VersionFailedError"`     | Version transitioned to `"failed"` status                 |
| `NoExistingVersionError` | `"NoExistingVersionError"` | `updateEnv` called on a service with no prior deployments |
| `CancelledError`         | `"CancelledError"`         | Operation cancelled via `AbortSignal`                     |
| `DestroyAggregateError`  | `"DestroyAggregateError"`  | Some versions failed during `destroyService`              |

### 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 | AuthenticationError | ApiError | BuildError | ArtifactError | TimeoutError | VersionFailedError`
- **`UpdateEnvError`** — errors from `updateEnv()`: `CancelledError | MissingArgumentError | AuthenticationError | ApiError | NoExistingVersionError | TimeoutError | VersionFailedError`
- **`DestroyVersionError`** — errors from `destroyVersion()`: `CancelledError | MissingArgumentError | AuthenticationError | ApiError | TimeoutError | VersionFailedError`
- **`DestroyServiceError`** — errors from `destroyService()`: `CancelledError | AuthenticationError | ApiError | DestroyAggregateError`
- **`ApiRequestError`** — errors from simple CRUD methods (`listProjects`, `listServices`, `createService`, etc.): `CancelledError | AuthenticationError | ApiError`

## Progress and interaction callbacks

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

### Deploy progress

```ts
await compute.deploy({
  strategy,
  projectId: "proj_abc",
  serviceName: "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`);
    },
    onVersionCreated(versionId) {
      console.log(`Version: ${versionId}`);
    },
    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}`);
    },
  },
});
```

### Deploy interaction

When `projectId`, `serviceId`, or `region` are not provided, the SDK calls interaction callbacks to let the consumer 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 selectService(services) {
      // services: ServiceInfo[] — return a service ID, or null to create a new one
      return null;
    },
    async provideServiceName() {
      // return a name for the new service
      return "my-new-service";
    },
    async selectRegion(regions) {
      // regions: RegionInfo[] — return the chosen region ID
      return "us-east-1";
    },
  },
});
```

### Destroy progress

```ts
await compute.destroyService({
  serviceId: "svc_xyz",
  progress: {
    onStoppingVersions(versionIds) {
      /* ... */
    },
    onVersionStopped(versionId) {
      /* ... */
    },
    onAllVersionsStopped() {
      /* ... */
    },
    onDeletingVersions(versionIds) {
      /* ... */
    },
    onVersionDeleted(versionId) {
      /* ... */
    },
    onAllVersionsDeleted() {
      /* ... */
    },
    onServiceDeleted(serviceId) {
      /* ... */
    },
  },
});
```

## 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,
  serviceId: "svc_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? }
  ServiceInfo, // { id, name, region, projectId, createdAt? }
  ServiceDetail, // ServiceInfo & { latestVersionId? }
  VersionInfo, // { id, status, createdAt, previewDomain? }
  VersionDetail, // VersionInfo & { envVars? }
  RegionInfo, // { id, displayName }
  ResolvedConfig, // { projectId, serviceId, serviceName, 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!,
    serviceName: "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(`Version ${e.versionId} may still be starting.`);
        process.exit(1);
      },
      _: (e) => {
        console.error(`Error: ${e.message}`);
        process.exit(1);
      },
    });
    return;
  }

  const { serviceId, versionId, deploymentUrl } = deployResult.value;
  console.log(`\nService: ${serviceId}`);
  console.log(`Version: ${versionId}`);
  console.log(`URL:     ${deploymentUrl}`);

  // List versions
  const versionsResult = await compute.listVersions({ serviceId });
  if (versionsResult.isOk()) {
    console.log(`\nVersions (${versionsResult.value.length}):`);
    for (const v of versionsResult.value) {
      console.log(`  ${v.id} — ${v.status} (${v.createdAt})`);
    }
  }
}

main();
```

## Requirements

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

## License

Apache-2.0
