DeployKit

Apps

One provider project per site, created exactly once.

An app is a project on the host: a Vercel project or a Pages project. Most SaaS products give each customer their own. Every deploy needs an app id.

Managing apps

import { Effect, Option } from "effect"
import { Deploykit } from "@deploykit/core"

const program = Effect.gen(function* () {
  const deploykit = yield* Deploykit.Deploykit
  const app = yield* deploykit.createApp("acme-site")
  yield* deploykit.getApp(app.id)
  yield* deploykit.deleteApp(app.id)

  // Option.none() if there isn't one.
  const existing = yield* deploykit.findAppByName("acme-site")
  if (Option.isSome(existing)) console.log(existing.value.id)
})

Store app.id against your customer as soon as createApp returns. Names must be unique on the host, so a retried createApp with the same name fails. Use findAppByName to pick up a project you created but didn't record.

The Platform service

Effect API only.

Creating an app per customer has a race: two requests for a new customer can both create a project. Platform solves that with a small store you provide, mapping your customer id to the host's app id. It also has a ready-made waitUntilReady.

src/platform.ts
import { Effect, Layer } from "effect"
import { NodeFileSystem } from "@effect/platform-node"
import { Artifact, Platform } from "@deploykit/core"
import * as Vercel from "@deploykit/vercel"
import { AppStoreLive } from "./appStore"

export const publish = (customerId: string, artifact: Artifact.Artifact) =>
  Effect.gen(function* () {
    const platform = yield* Platform.Platform

    const app = yield* platform.apps.getOrCreate({
      externalId: customerId,
      name: `site-${customerId}`
    })
    const deployment = yield* platform.deploy(app, { artifact })
    return yield* platform.deployments.waitUntilReady(app.id, deployment.id)
  })

// Platform sits on the raw provider layer, not on Deploykit.
export const PlatformLive = Platform.layer.pipe(
  Layer.provide(Vercel.vercelLayer),
  Layer.provide(AppStoreLive),
  Layer.provide(NodeFileSystem.layer)
)
MethodDoes
apps.getOrCreate({ externalId, name })The customer's app, creating it at most once
apps.get(appId)Look up an app
apps.delete({ externalId, appId })Delete the project and forget the mapping
apps.setAccess(appId, access)Present only if the provider supports access control
deploy(app, { artifact, ...options })Deploy
deployments.get(appId, deploymentId)Read a deployment
deployments.waitUntilReady(appId, id, opts?)Poll until final
capabilitiesWhat the provider supports

getOrCreate checks your store first, then adopts an existing project with that name if the host can find one, and only then creates a new one. If it loses a race, it deletes the project it just made and returns the winner's.

waitUntilReady polls every second for up to five minutes and tolerates three failed polls in a row. Pass { schedule, tolerateFailures } to change that. If the build is still running at the end it fails with DeploymentTimeoutError, which includes lastStatus.

platform.deploy goes straight to the provider, so it doesn't share the Deploykit layer's budgets. To keep one budget across everything, use Platform for apps and Deploykit for deploys, over the same provider layer.

Writing the app store

Back it with your database. Each method returns an Effect; report database failures as Platform.AppStoreError.

src/appStore.ts
import { Layer } from "effect"
import { Platform } from "@deploykit/core"

export const AppStoreLive = Layer.succeed(Platform.TenantAppStore, {
  // Option.some(appId) if this customer has an app, otherwise Option.none().
  get: externalId => db.findAppId(externalId),

  // Save only if nothing is saved yet. Must be atomic.
  // Succeed with { _tag: "Stored" } or { _tag: "AlreadyRecorded", appId }.
  put: (externalId, appId) => db.insertIfAbsent(externalId, appId),

  // Remove the mapping. Removing one that isn't there is not an error.
  forget: externalId => db.deleteAppId(externalId)
})

put must be atomic, using a unique constraint or a conditional write. That's the one guarantee deploykit can't provide itself. For tests, use MemoryAppStore.

Last updated on

On this page