---
title: "Move your vals off Val Town"
description: "Val Town shuts down on January 10, 2027. Move your HTTP, cron, and email vals, with their SQLite, blobs, and secrets, to one exe.dev VM, same code."
url: https://exe-staging.dev/deploy/migrate-val-town
category: "Migration"
updated: 2026-11-13
---

# Move your vals off Val Town

Every val on one VM, same code, with its SQLite, blobs, and secrets.

- **Software:** Vals from Val Town (https://www.val.town)
- **Source:** https://github.com/denoland/deno
- **License:** MIT (Deno); your vals are yours
- **Time:** 1 hour
- **Example VM name:** `my-vals` (yours will differ; on the VM, `hostname` prints it)
- **Machine-readable:** https://exe-staging.dev/deploy/migrate-val-town/manifest.json
- **File:** https://exe-staging.dev/deploy/migrate-val-town/blob-global.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/blob-store.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/blob.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/email.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/export-data.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/export-vals.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/front.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/imports.json
- **File:** https://exe-staging.dev/deploy/migrate-val-town/mail-router.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/openai.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/prepare.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/pull-data.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/pull-env.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/run.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/sqlite-global.ts
- **File:** https://exe-staging.dev/deploy/migrate-val-town/sqlite.ts

## What you get

- Every val's code exported to `~/valtown-export`, with each file's trigger and the names of its environment variables.
- HTTP vals running as services that restart on reboot, each on its own private port, and the ones you choose at `https://<vm>.exe.xyz/<val>/`.
- Cron vals on systemd timers, in UTC, with `lastRunAt` carried from run to run.
- Email vals answering mail to `<val>@<vm>.exe.xyz`.
- std/sqlite, std/blob, std/email, and std/openai working on the VM without code changes.
- A report of each val's schedule, secrets, and data to bring over, and any line still calling Val Town.

Running on the VM:

- **Deno 2.9**: Runs each val's code as it ran on Val Town
- **The val kit**: Runs triggers, and stands in for std/sqlite, std/blob, std/email, and std/openai, data in `~/vals-kit`
- **Your vals**: One directory per val, with its code, `.env`, and data, data in `~/vals/<val>`
- **systemd**: A service per HTTP val, a timer per cron val, the mail router, and the front door, port 8000

How it is reached:

- https://my-vals.exe.xyz/<val>/ (private): HTTP vals with `PUBLIC=1`, through the front door; public once you share the VM
- https://my-vals.exe.xyz:8001/ (private): one HTTP val on its own port (8001, 8002, …), for you only
- <val>@my-vals.exe.xyz (public): the address of an email val, once you turn on receiving email

## Deploy with Shelley

Open https://exe-staging.dev/new?prompt=VAL_TOWN_API_KEY%3D%3Cfill-this-in%3E%0AVALS%3D%3Cfill-this-in%3E%0A%0ASet%20up%20Vals%20from%20Val%20Town%20on%20this%20VM%20by%20following%20the%20exe.dev%20recipe%20at%20https%3A%2F%2Fexe-staging.dev%2Fdeploy%2Fmigrate-val-town.md%20from%20top%20to%20bottom.%0A%0AThe%20values%20at%20the%20top%20of%20this%20message%20are%20mine%3A%0A-%20VAL_TOWN_API_KEY%3A%20a%20Val%20Town%20API%20token%20with%20read%20access%20to%20vals%20%28Settings%20%E2%86%92%20API%20Tokens%29%3B%20only%20to%20list%20and%20export%20your%20vals%2C%20and%20leave%20it%20out%20to%20move%20only%20public%20vals%0A-%20VALS%3A%20which%20vals%20to%20move%2C%20written%20%60user%2Fval%60%3B%20leave%20empty%20for%20all%20of%20them%0AAsk%20me%20for%20any%20that%20still%20say%20%3Cfill-this-in%3E%20before%20you%20need%20them.%0A%0AThe%20recipe%27s%20example%20VM%20name%20is%20my-vals.%20This%20VM%27s%20name%20is%20the%20output%20of%20%60hostname%60%3A%20use%20it%20wherever%20the%20recipe%20writes%20my-vals.%20Skip%20the%20step%20that%20creates%20the%20VM%3B%20you%20are%20on%20it.%0A%0AYou%20are%20done%20when%20all%20of%20these%20are%20true.%20Check%20each%20one%20yourself%3A%0A-%20Every%20val%27s%20code%20exported%20to%20%60~%2Fvaltown-export%60%2C%20with%20each%20file%27s%20trigger%20and%20the%20names%20of%20its%20environment%20variables.%0A-%20HTTP%20vals%20running%20as%20services%20that%20restart%20on%20reboot%2C%20each%20on%20its%20own%20private%20port%2C%20and%20the%20ones%20you%20choose%20at%20%60https%3A%2F%2F%3Cvm%3E.exe.xyz%2F%3Cval%3E%2F%60.%0A-%20Cron%20vals%20on%20systemd%20timers%2C%20in%20UTC%2C%20with%20%60lastRunAt%60%20carried%20from%20run%20to%20run.%0A-%20Email%20vals%20answering%20mail%20to%20%60%3Cval%3E%40%3Cvm%3E.exe.xyz%60.%0A-%20std%2Fsqlite%2C%20std%2Fblob%2C%20std%2Femail%2C%20and%20std%2Fopenai%20working%20on%20the%20VM%20without%20code%20changes.%0A-%20A%20report%20of%20each%20val%27s%20schedule%2C%20secrets%2C%20and%20data%20to%20bring%20over%2C%20and%20any%20line%20still%20calling%20Val%20Town.%0A%0ARead%20the%20whole%20recipe%2C%20including%20the%20closed%20details%2C%20and%20follow%20%22Move%0Athe%20vals%20by%20hand%22%20in%20order%2C%20skipping%20VM%20creation%3B%20the%20walkthrough%20above%0Ait%20is%20for%20the%20owner.%20Install%20exactly%20the%20pinned%20Deno%20version.%0APut%20the%20API%20key%20in%20~%2F.valtown-api-key%20%28mode%20600%29%20and%20keep%20it%2C%20the%0Aexport%20token%2C%20and%20every%20secret%20value%20out%20of%20your%20replies%20and%20out%20of%0AGit.%20Never%20change%20anything%20on%20Val%20Town%20yourself%3A%20adding%20export-data.ts%2C%0Asetting%20variables%2C%20pausing%20vals%2C%20and%20redirects%20are%20the%20owner%27s%20steps.%0AAsk%20the%20owner%20for%20each%20cron%20val%27s%20schedule%20%28the%20API%20doesn%27t%20return%20it%29.%0ADon%27t%20run%20share%20set-public%20or%20share%20receive-email%3B%20give%20the%20owner%0Athose%20commands.%20Leave%20vals%20whose%20secrets%20are%20missing%20stopped.%0AIf%20a%20val%20uses%20a%20Val%20Town%20feature%20this%20kit%20doesn%27t%20replace%2C%20list%20it%20in%0Athe%20report%20rather%20than%20rewriting%20the%20val.%0AFinish%20with%20the%20report%20from%20%22Check%22%3A%20per%20val%2C%20its%20triggers%2C%20port%2C%0Aschedule%2C%20email%20address%2C%20missing%20secrets%2C%20data%20still%20to%20copy%2C%20and%0Aprepare.ts%27s%20leftover%20lines%3B%20then%20the%20owner%27s%20steps%20in%20%22Make%20the%20move%22.%0A%0AFinish%20with%20the%20URLs%20I%20should%20open.%0A to create a VM whose agent, Shelley, follows this recipe. From a terminal:

```sh
curl -fsSL https://exe-staging.dev/deploy/migrate-val-town/prompt.txt | ssh exe.dev new --prompt=/dev/stdin
```

Or follow the steps below by hand.

Val Town is [winding down](https://blog.val.town/winding-down): code goes
read-only on December 10, 2026, and vals stop on January 10, 2027. This
recipe moves your vals to one exe.dev VM without rewriting them. HTTP vals
become services behind exe.dev's HTTPS proxy, cron vals become systemd
timers, and email vals answer mail at `<val>@<vm>.exe.xyz`. A small kit
stands in for Val Town's standard library, so `std/sqlite` and `std/blob`
write to files on the VM, `std/email` sends through exe.dev's email
gateway, and `std/openai` goes through exe.dev's LLM gateway, with no API
keys.

> [!NOTE]
> Two steps need Val Town before **December 10**: adding the data export
> file to your vals (your code is read-only after that), and setting
> `*.val.run` redirects. Copying the data itself can wait until January 9.

Examples move three vals: `guestbook`, an HTTP val with a Hono app and
`std/sqlite`; `digest`, a cron val that emails new Hacker News stories and
remembers them in `std/blob`; and `inbox`, an email val that files each
message in SQLite and mails you a one-line summary from `std/openai`. Use
your own names.

## How the move works

1. **Shelley exports your code** with Val Town's API and sets up every val
   on the VM. Val Town keeps running; nothing there changes.
2. **You add one file to each val that keeps data,** and Shelley copies its
   SQLite tables, blobs, and secrets, then counts both sides. You try the
   vals on the VM.
3. **The move:** you pause the vals on Val Town, Shelley copies the data
   again, and you redirect `*.val.run` and update webhooks and email
   senders.

On the VM, each val gets a directory:

| Path | What it holds |
|---|---|
| `~/vals/guestbook/` | The val's files, as on Val Town |
| `~/vals/guestbook/.env` | Its secrets, and which file runs for which trigger |
| `~/vals/guestbook/deno.json` | Imports that send `std/*` to the kit |
| `~/vals/guestbook/data/` | `sqlite.db` and `blobs.db`, the val's own |
| `~/vals/.global/` | The account-wide `std/sqlite` and `std/blob`, shared |

| On Val Town | On the VM |
|---|---|
| HTTP trigger, `*.val.run` | `val-http@<val>` service on a private port; `https://<vm>.exe.xyz/<val>/` with `PUBLIC=1` |
| Cron trigger | `val-cron@<val>.timer`, in UTC |
| Email trigger, `@valtown.email` | Mail to `<val>@<vm>.exe.xyz`, through the mail router |
| Environment variables | The val's `.env`, read by systemd |
| `std/sqlite` | SQLite file, same `@libsql/client` API |
| `std/blob` | SQLite file, same `get`, `set`, `list`, `getJSON` |
| `std/email` | exe.dev's [email gateway](/docs/send-email): plain text, to you and people who mail the VM |
| `std/openai` | exe.dev's [LLM gateway](/docs/integrations-llm), no key |
| Logs | `journalctl -u val-http@<val>` |

## Look around

When Shelley finishes, open `https://my-vals.exe.xyz:8001/` and sign in
to exe.dev: that's your first HTTP val, on its own port. Vals with
`PUBLIC=1` are also at `https://my-vals.exe.xyz/<val>/`, private until
you share the VM. Run a cron val now instead of waiting:

```bash
sudo systemctl start val-cron@digest
journalctl -u val-cron@digest -n 20
```

To try an email val, turn on receiving email, on your computer:

```bash
ssh exe.dev share receive-email my-vals on
```

Then mail `inbox@my-vals.exe.xyz` and watch `journalctl -u vals-mail -f`
on the VM. These are your vals with your secrets: anything they do
elsewhere, like posting to Slack, really happens.

## Make the move

First, before December 10:

- In each HTTP val's settings on Val Town, set its redirect to the new
  address, `https://my-vals.exe.xyz/<val>` or your domain. Once
  redirected, the val on Val Town stops getting requests, so do this when
  you're ready. Redirects keep the path and query, and become permanent on
  December 10.
- Make sure `export-data.ts` is in every val whose data you still need to
  copy (see [Copy the data](#copy-the-data)).

Then, any time before January 10, for each val with data:

1. On Val Town, stop the val writing: once its redirect is set it gets no
   HTTP requests; remove its cron and email triggers too. Leave
   `export-data.ts` running. If your code is already read-only, copy as
   close to January 10 as you can instead.
2. On the VM, stop the val, copy the data again, and start it:

```bash
sudo systemctl stop val-http@guestbook
EXPORT_TOKEN=$(cat ~/vals/guestbook/.export-token) \
  deno run -A ~/vals-kit/pull-data.ts https://<export-data URL> ~/vals/guestbook
sudo systemctl start val-http@guestbook
```

Expect `pull-data: all counts match`. Then, on your computer, open the
front door to everyone and, if you use one, add your domain
([custom domains](/docs/cnames)):

```bash
ssh exe.dev share set-public my-vals
ssh exe.dev domain add my-vals app.example.com
```

Update webhooks (Stripe, GitHub, Slack, Telegram…) to the new URLs, since
most don't follow redirects, and give email senders the new addresses.

## Day to day

Add a val: make `~/vals/<name>`, write its code and `.env`, run
`prepare.ts`, and start its unit as in [the nuts and
bolts](#set-up-each-val). Edit code in place, then restart:
`sudo systemctl restart val-http@guestbook`. Cron and email vals pick up
changes on their next run.

Watch everything at once with `journalctl -f -u 'val-*' -u 'vals-*'`, and
see every timer with `systemctl list-timers 'val-cron@*'`. Back up
`~/vals`; the SQLite files are in WAL mode, so copy them with
`sqlite3 data/sqlite.db ".backup /path/copy.db"` while the val runs.

## Clean up

Once every val has run well on the VM for a while:

- On Val Town, delete `export-data.ts` and the `EXPORT_TOKEN` variable from
  each val, and revoke the API token.
- On the VM, delete `~/.valtown-api-key` and every `~/vals/*/.export-token`.
- Keep `~/valtown-export`, and the `*.old` files `pull-data.ts` leaves,
  until you're sure.

## The nuts and bolts

These are the steps Shelley follows, in order.

<details id="create-the-vm" data-manual-setup class="dp-details">
<summary>Move the vals by hand</summary>

### Create the VM

On your computer:

```bash
ssh exe.dev new --name=my-vals
ssh my-vals.exe.xyz
```

### Install Deno and the kit

On the VM:

```bash
curl -fsSL https://deno.land/install.sh | sh -s -- -y v2.9.8
export PATH="$HOME/.deno/bin:$PATH"
mkdir -p ~/vals-kit ~/vals && cd ~/vals-kit
for f in export-vals.ts export-data.ts pull-data.ts pull-env.ts prepare.ts \
    imports.json run.ts front.ts mail-router.ts sqlite.ts sqlite-global.ts \
    blob-store.ts blob.ts blob-global.ts email.ts openai.ts; do
  curl -fsSLO https://exe-staging.dev/deploy/migrate-val-town/$f
done
```

| File | What it does |
|---|---|
| [export-vals.ts](https://exe-staging.dev/deploy/migrate-val-town/export-vals.ts) | Copies vals' code with Val Town's API, and lists their triggers and variable names |
| [prepare.ts](https://exe-staging.dev/deploy/migrate-val-town/prepare.ts) | Writes a val's `deno.json` from [imports.json](https://exe-staging.dev/deploy/migrate-val-town/imports.json), and lists lines that still use Val Town |
| [run.ts](https://exe-staging.dev/deploy/migrate-val-town/run.ts) | Runs a val's HTTP, cron, or email handler, the way Val Town called it |
| [front.ts](https://exe-staging.dev/deploy/migrate-val-town/front.ts) | The front door on port 8000: `/<val>/` and custom domains to vals with `PUBLIC=1` |
| [mail-router.ts](https://exe-staging.dev/deploy/migrate-val-town/mail-router.ts) | Hands each email in `~/Maildir/new` to the val it's addressed to |
| [export-data.ts](https://exe-staging.dev/deploy/migrate-val-town/export-data.ts) | Goes in a val on Val Town: read-only, token-protected access to its data |
| [pull-data.ts](https://exe-staging.dev/deploy/migrate-val-town/pull-data.ts) | Copies a val's SQLite and blobs through `export-data.ts`, and counts both sides |
| [pull-env.ts](https://exe-staging.dev/deploy/migrate-val-town/pull-env.ts) | Copies named environment variables through `export-data.ts` into `.env` |
| [sqlite.ts](https://exe-staging.dev/deploy/migrate-val-town/sqlite.ts), [sqlite-global.ts](https://exe-staging.dev/deploy/migrate-val-town/sqlite-global.ts) | `std/sqlite`, on `data/sqlite.db` and `~/vals/.global/sqlite.db` |
| [blob.ts](https://exe-staging.dev/deploy/migrate-val-town/blob.ts), [blob-global.ts](https://exe-staging.dev/deploy/migrate-val-town/blob-global.ts), [blob-store.ts](https://exe-staging.dev/deploy/migrate-val-town/blob-store.ts) | `std/blob`, in `data/blobs.db` and `~/vals/.global/blobs.db` |
| [email.ts](https://exe-staging.dev/deploy/migrate-val-town/email.ts) | `std/email`, through exe.dev's email gateway |
| [openai.ts](https://exe-staging.dev/deploy/migrate-val-town/openai.ts) | `std/openai`, through exe.dev's LLM gateway |

Install the four systemd units every val shares:

```bash
sudo tee /etc/systemd/system/val-http@.service >/dev/null <<'EOF'
[Unit]
Description=Val %i (HTTP)
After=network-online.target

[Service]
User=exedev
WorkingDirectory=/home/exedev/vals/%i
EnvironmentFile=/home/exedev/vals/%i/.env
ExecStart=/home/exedev/.deno/bin/deno run --allow-all --config=deno.json /home/exedev/vals-kit/run.ts http
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF
sudo tee /etc/systemd/system/val-cron@.service >/dev/null <<'EOF'
[Unit]
Description=Val %i (cron)

[Service]
Type=oneshot
User=exedev
WorkingDirectory=/home/exedev/vals/%i
EnvironmentFile=/home/exedev/vals/%i/.env
ExecStart=/home/exedev/.deno/bin/deno run --allow-all --config=deno.json /home/exedev/vals-kit/run.ts cron
EOF
sudo tee /etc/systemd/system/vals-mail.service >/dev/null <<'EOF'
[Unit]
Description=Email for vals
After=network-online.target

[Service]
User=exedev
ExecStart=/home/exedev/.deno/bin/deno run --allow-all /home/exedev/vals-kit/mail-router.ts
Restart=always

[Install]
WantedBy=multi-user.target
EOF
sudo tee /etc/systemd/system/vals-front.service >/dev/null <<'EOF'
[Unit]
Description=Front door for vals
After=network-online.target

[Service]
User=exedev
ExecStart=/home/exedev/.deno/bin/deno run --allow-all /home/exedev/vals-kit/front.ts
Restart=always

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now vals-front vals-mail
```

### Export the code

Save your API key where only you can read it, then export every val, or
name the ones you want:

```bash
(umask 077; cat > ~/.valtown-api-key)   # paste the token, then Ctrl-D
deno run -A ~/vals-kit/export-vals.ts
deno run -A ~/vals-kit/export-vals.ts alice/guestbook alice/digest alice/inbox
```

Each val's files land in `~/valtown-export/<val>/`. The script prints each
trigger, and `~/valtown-export/vals.json` keeps them with each val's
variable names:

```
guestbook: 2 files
  http  main.ts https://alice--4f2e9c1a.web.val.run
digest: 2 files
  cron  main.ts
inbox: 2 files
  email main.ts alice-7b3d0e52@valtown.email
```

Val Town's API doesn't return cron schedules: read each one from the
val's trigger settings.

### Set up each val

Copy a val into `~/vals`, under a lowercase name of letters, digits, `-`
and `_` (it becomes the path and the email address), and point its
`std` imports at the kit:

```bash
cp -r ~/valtown-export/guestbook ~/vals/guestbook
deno run -A ~/vals-kit/prepare.ts ~/vals/guestbook
```

`prepare.ts` ends with `nothing left that uses Val Town`, or lists lines to
look at, such as imports of other vals (`esm.town/v/alice/...`; copy
those vals in and import them by path) or `*.val.run` URLs.

Then write the val's `.env`. These variables tell the kit what to run;
the val's own secrets go below them:

| Variable | Meaning |
|---|---|
| `VAL_NAME` | The val's name, for the default email subject |
| `VAL_HTTP` | The file with the HTTP handler |
| `PORT` | Its private port, from 8001 up, one per HTTP val (3000-9999 are reachable at `https://<vm>.exe.xyz:PORT/`) |
| `PUBLIC` | `1` to serve it at `https://<vm>.exe.xyz/<val>/` |
| `HOSTS` | Custom domains for it, comma-separated |
| `VAL_CRON` | The file with the cron handler |
| `VAL_EMAIL` | The file with the email handler |

One val can have all three triggers. For the examples:

```bash
printf 'VAL_NAME=guestbook\nVAL_HTTP=main.ts\nPORT=8001\nPUBLIC=1\n' > ~/vals/guestbook/.env
printf 'VAL_NAME=digest\nVAL_CRON=main.ts\n' > ~/vals/digest/.env
printf 'VAL_NAME=inbox\nVAL_EMAIL=main.ts\n' > ~/vals/inbox/.env
chmod 600 ~/vals/*/.env
```

**HTTP vals** start with one command each:

```bash
sudo systemctl enable --now val-http@guestbook
sleep 5   # the first start downloads the val's packages
curl -s localhost:8001/
```

The handler gets a `Request` whose URL has the public host, as on
`val.run`. Behind the front door at `/guestbook/`, paths still start at
`/` (the prefix is in the `X-Forwarded-Prefix` header), which suits APIs
and webhooks; a site with links like `href="/about"` wants its own
domain in `HOSTS`.

**Cron vals** get a timer each. Translate the schedule:

| Val Town cron | `OnCalendar=` |
|---|---|
| `*/15 * * * *` | `*:0/15` |
| `0 * * * *` | `hourly` |
| `0 9 * * *` | `*-*-* 09:00:00 UTC` |
| `30 8 * * 1-5` | `Mon..Fri *-*-* 08:30:00 UTC` |
| `0 0 1 * *` | `*-*-01 00:00:00 UTC` |
| Every 3 hours (interval) | `0/3:00` |

```bash
sudo tee /etc/systemd/system/val-cron@digest.timer >/dev/null <<'EOF'
[Unit]
Description=Val digest (cron */15 * * * *)

[Timer]
OnCalendar=*:0/15
Persistent=true

[Install]
WantedBy=timers.target
EOF
systemd-analyze calendar '*:0/15'
sudo systemctl daemon-reload
sudo systemctl enable --now val-cron@digest.timer
```

`systemd-analyze` prints the next run, to check the translation. The
handler gets `{ lastRunAt }`, kept in `data/cron-last-run`.
`Persistent=true` catches up a run missed while the VM was off.

**Email vals** need only their `.env`. Mail to `inbox@<vm>.exe.xyz`, or
`inbox+anything@`, goes to `~/vals/inbox`; the handler gets Val Town's
`Email` object, with `attachments` as `File`s. Messages land in
`~/Maildir/cur` once handled, or `~/Maildir/failed`. exe.dev takes
messages up to 1 MB, with no spam filtering. The owner turns receiving on:
`ssh exe.dev share receive-email my-vals on`.

### Copy the data

A val's SQLite database and blobs can only be read from inside the val, so
the owner adds one file to each val that keeps data, in Val Town:

1. On the VM, make a token: `(umask 077; openssl rand -hex 32 >
   ~/vals/guestbook/.export-token)`, and show it with `cat`.
2. In the val on Val Town, add an environment variable `EXPORT_TOKEN`
   with that value, and a new HTTP file named `export-data.ts` with the
   contents of
   [export-data.ts](https://exe-staging.dev/deploy/migrate-val-town/export-data.ts).
   Copy the file's URL.

Then copy:

```bash
EXPORT_TOKEN=$(cat ~/vals/guestbook/.export-token) \
  deno run -A ~/vals-kit/pull-data.ts https://<export-data URL> ~/vals/guestbook
```

```
table entries: 1202 rows here, 1202 on Val Town
integrity_check: ok
blobs: 2 (12 bytes)
pull-data: all counts match
```

Tables, indexes, triggers, views, and `AUTOINCREMENT` counters come
across with every value exact (64-bit integers, blobs, text with NULs).
Virtual tables such as FTS are skipped and named: rebuild them from
their source table. For the account-wide `std/sqlite` and `std/blob`, add
`--global` and use `~/vals/.global` as the directory; any one val's
`export-data.ts` serves them. Run it again whenever you like: earlier
files are kept as `*.old`.

### Copy the secrets

`vals.json` lists each val's variable names. Copy their values through
the same file, without printing them:

```bash
EXPORT_TOKEN=$(cat ~/vals/guestbook/.export-token) \
  deno run -A ~/vals-kit/pull-env.ts https://<export-data URL> ~/vals/guestbook SLACK_TOKEN GITHUB_TOKEN
```

It won't copy `EXPORT_TOKEN` or Val Town's own `valtown` key. For a val
without `export-data.ts`, copy values from its settings into `.env` with
an editor: `NAME=value`, or `NAME='value'` with spaces or `$`. Restart
the val after.

Keys the val only sends to one API can stay off the VM: an exe.dev
[HTTP proxy integration](/docs/integrations-http-proxy) adds the key on
the way out. On your computer:

```bash
ssh exe.dev integrations add http-proxy --name github \
  --target https://api.github.com --bearer "$GITHUB_TOKEN" --attach vm:my-vals
```

Then change the val to call `https://github.int.exe.xyz/...` with no key.

### Check

```bash
systemctl --no-pager is-active vals-front vals-mail 'val-http@*'
systemctl list-timers 'val-cron@*' --no-pager
for d in ~/vals/*/; do deno run -A ~/vals-kit/prepare.ts "$d" | tail -1; done
```

For the report, per val: its triggers, port and address, schedule and
next run, email address, secrets named in `vals.json` but not in `.env`,
whether its data is copied, and what `prepare.ts` still lists.

</details>

<details id="troubleshooting" class="dp-details">
<summary>Troubleshooting</summary>

**`has no default export`.** Val Town runs a file's default export, or its
only export. Add `export default` to the handler.

**`Email` or `Interval` is not defined.** Those are type names from Val
Town's editor; `deno run` doesn't type-check, so this only comes up in
`deno check`. Declare them in the file or drop the annotation.

**An email val gets nothing.** Run `ssh exe.dev share receive-email
my-vals on` again, check that the val's directory name matches the
address, and read `journalctl -u vals-mail`. exe.dev stops delivering if `~/Maildir/new`
passes 1,000 messages, so keep `vals-mail` running.

**`exe.dev email error`.** The gateway only sends to you, your team,
people you share the VM with, and people who've emailed it. HTML is
turned into plain text; `from` and custom headers are dropped.

**`std/openai` says the model doesn't exist.** Use a model the [LLM
gateway](/docs/integrations-llm) offers.

**`pull-data` says MISMATCH.** Something wrote during the copy: pause the
val on Val Town and run it again.

</details>

## Frequently asked questions

### When does Val Town shut down?

New signups ended on November 10, 2026. On December 10 your code goes read-only, though vals keep running and SQLite and blobs stay writable. On January 10, 2027 everything else stops. Val Town plans to open-source itself on February 1, 2027. See [the announcement](https://blog.val.town/winding-down).

### Do I have to rewrite my vals?

Usually not. They're Deno modules, and Deno runs them on the VM. A `deno.json` beside each val sends its `esm.town/v/std/...` imports to the kit, so `sqlite`, `blob`, `email`, and `OpenAI` keep their methods. `prepare.ts` lists anything left, such as vals importing other vals by URL, which stop working on January 10.

### Isn't Deno winding down too?

Its team [joined Cloudflare](https://deno.com/blog/cloudflare) and promises a year of security and bug-fix releases, to about October 2027. A Deno binary on your VM keeps working after that, but plan to port vals to Node or Bun within the year; the kit's files are small and the ports are mostly mechanical.

### How many vals fit on one VM?

Dozens. An idle HTTP val is one Deno process of about 60 MB; cron and email vals use memory only while they run. All of them share your [VM plan](https://exe.dev/pricing).

### What about my *.val.run links and webhooks?

Before December 10, set a redirect for each HTTP val in Val Town (val settings, or **Custom Domains** for the account) to its new address; on December 10 they become permanent. Browsers and links follow them; most webhook senders don't, so update those to the new URL directly. Email to `@valtown.email` addresses can't be forwarded: tell senders the new address.

### Can I use my own domain?

Yes. Point a CNAME at `my-vals.exe.xyz`, run `ssh exe.dev domain add my-vals app.example.com`, and add the domain to the val's `HOSTS`. Email still goes to `@my-vals.exe.xyz` addresses: exe.dev doesn't receive mail for custom domains.

### What about smallweb or Windmill?

[smallweb](https://www.smallweb.run) runs a folder of Deno apps, one subdomain each, close to how Val Town feels; on exe.dev it needs a wildcard custom domain (`*.example.com`). Windmill suits vals that are really workflows. This recipe keeps your code as it is and needs neither.


## Related

- [Migrate a Heroku app to exe.dev](https://exe-staging.dev/deploy/migrate-heroku.md): Your Procfile, config vars, and Postgres on a VM of your own.
- [Migrate a Railway project to exe.dev](https://exe-staging.dev/deploy/migrate-railway.md): Your Railway services and data on one VM, and your API keys off it.
- [Migrate a Durable Object from Cloudflare](https://exe-staging.dev/deploy/migrate-durable-objects.md): Same Worker, same objects, new home. Every key and row checked.
- [Move a Lovable app and its data to exe.dev](https://exe-staging.dev/deploy/migrate-lovable.md): Your Lovable app, its database, and its users, on a server of your own.
- [Migrate a DigitalOcean Droplet to exe.dev](https://exe-staging.dev/deploy/migrate-digitalocean-droplet.md): Same server, new home. Rehearsed while the droplet keeps serving.
- [Move from Conductor Cloud to your own always-on VM](https://exe-staging.dev/deploy/migrate-conductor-cloud.md): Conductor Cloud closes Nov 20. Keep your agents running on a VM of your own.
