---
title: Customer onboarding with Stripe, email and SQL
---

# Customer onboarding with Stripe, email and SQL

Create a Stripe customer, save the application's signup row in PostgreSQL, and request a welcome email with Resend. Inspect each stored outcome through the real SDKs. The browser form and command-line workflow use the same application code.

You need Node 22.6+, npm and registry access during installation. The browser page also prepares public Volter brand assets before entering the World. No platform account or real vendor credential is needed. PostgreSQL is a World-managed application database; its records are application state, separate from SaaS history.

## Obtain the application

Copy this starter into a new folder:

```console
$ npx @volter/world@3.0.158 example customer-onboarding --dir onboarding
$ cd onboarding
$ npm install
```

The command copies source; it neither installs dependencies nor starts a World. Its manifest pins the CLI, core, runtime, console, twins and SDKs. The installed starter creates a lockfile during installation. The repository's [package manifest](./package.json) and [lockfile](./package-lock.json) identify the source example's dependency cohort. Keep your own lockfile with the application when sharing it.

Read [onboarding.mjs](./onboarding.mjs) for the application and [run.mjs](./run.mjs) for the command entry. Empty keys in [.env.example](./.env.example) tell init what the app reads; init supplies throwaway credentials. Do not add real keys.

```console
$ ./node_modules/.bin/volter world init --name onboarding --twins stripe,resend
```

Review Stripe and Resend's exact sources in `.volter/world.json`. The `pg` dependency and `DATABASE_URL` also request a managed PostgreSQL service. Keep it: this app needs SQL. SaaS selection does not exclude a declared native database. `world covers` describes the configured database as part of this workflow.

## Create and read an account

```console
$ ./node_modules/.bin/volter world up
$ ./node_modules/.bin/volter world clock set 2026-01-15T12:00:00Z
$ ./node_modules/.bin/volter world run -- node run.mjs create Ada owner@world.test
$ ./node_modules/.bin/volter world clock advance 5s
$ ./node_modules/.bin/volter world run -- node run.mjs read
$ ./node_modules/.bin/volter world log
```

Read the customer returned by Stripe, the signup row returned by SQL and the email returned by Resend. `completion: complete` means delivery was observed, not merely that a send request was accepted. Opaque IDs can differ between fresh Worlds; customer identity, stored fields and delivery status are the useful outcomes.

## Observe two different failures

A refusing recipient's mail server creates a bounce after the send was accepted:

```console
$ ./node_modules/.bin/volter world run -- node run.mjs bounce
$ ./node_modules/.bin/volter world run -- node run.mjs create Grace owner@world.test
$ ./node_modules/.bin/volter world clock advance 5s
$ ./node_modules/.bin/volter world run -- node run.mjs read
```

Grace's customer and SQL row remain. Her email exists and has `last_event: bounced`; the application reports `partial-email-bounced`. The `bounce` command uses Resend's declared synthetic recipient setting, not a successful-send handler. Restore delivery before later signups:

```console
$ ./node_modules/.bin/volter world run -- node run.mjs deliver
```

An unverified sender is refused before an email is accepted:

```console
$ ./node_modules/.bin/volter world run -- node run.mjs refused
$ ./node_modules/.bin/volter world run -- node run.mjs retry
$ ./node_modules/.bin/volter world clock advance 5s
$ ./node_modules/.bin/volter world run -- node run.mjs read
```

The first command creates Lin's customer and SQL row, then records the email refusal. The second resumes that pending welcome on the same customer. It refuses to resend an email that already has an accepted ID. These application receipts do not guarantee exactly-once delivery after an ambiguous network failure; resolve acceptance before retrying an uncertain send.

## Use the browser form

Prepare its fonts and logo outside the World:

```console
$ node prepare-ui.mjs
```

Then start the app through the World:

```console
$ ./node_modules/.bin/volter world run -- node server.mjs
```

Open the printed app URL in your browser. Submit a signup, advance the World clock in another terminal and reload. The page shows the completion state and full stored outcome. Its server registers its app URL with the World after listening; consumers can discover it with `world app-url`.

To open the World dashboard, stop the app with Ctrl-C first, then switch the owned compute to the viewer:

```console
$ ./node_modules/.bin/volter world down
$ ./node_modules/.bin/volter world view --no-open
```

The viewer prints the dashboard URL and keeps running. Start `world run -- node server.mjs` in a second terminal if you want the application alongside it. Open **Stripe** under **Records**, then select the customer to inspect its name, email and stored ID. Open **Resend** to inspect the accepted email and delivery event. **Board** opens the vendor pages declared by the installed twin; its root workspace may cover settings rather than customers. Provisioning remains in full history even when hidden from the business-activity view. Read the application's SQL completion record with `run.mjs read` or its browser page.

Stop app consumers before stopping the viewer. `down` retains vendor and declared application state. [Resume](../../docs/guides/resume-a-world.md) explains continuing it; [demo a workflow](../../docs/guides/demo-a-workflow.md) uses this same application for a repeatable baseline, refusal and retry. Reset only your disposable example World; it discards that branch's work.

[Source files](./public-files.json) are also in the CLI starter. This page describes the source workflow; it does not inherit execution evidence from another example or an older release.
