val-town

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Val Town

Val Town

Overview

概述

Val Town is a serverless platform that runs TypeScript/TSX on Deno. A val is a versioned folder of files (now multi-file by default) that can expose HTTP, Cron, or Email triggers. Every val gets a private SQLite database, blob storage, and free access to cheap OpenAI models.
Core mental model: Deno + multi-file project + suffix-based trigger detection. Edit locally with
vt
, watch syncs to production.
Val Town 是一个运行 TypeScript/TSX 的无服务器 Deno 平台。val 是一个带版本控制的文件目录(默认支持多文件),可以暴露 HTTP、定时任务(Cron)或邮件触发器。每个 val 都配有私有 SQLite 数据库、Blob 存储,并且可免费访问低成本的 OpenAI 模型。
核心思维模型: Deno + 多文件项目 + 基于后缀的触发器检测。使用
vt
在本地编辑,实时同步到生产环境。

Quick Reference

快速参考

TaskApproach
Create val
vt create my-val && cd my-val
Clone val
vt clone username/valName
Live sync
vt watch
(preferred dev loop)
One-shot push
vt push
Browse online
vt browse
Tail logs
vt tail
HTTP endpointfilename ends in
.http.tsx
(or
.http.ts
)
Cron jobfilename ends in
.cron.tsx
Email handlerfilename ends in
.email.tsx
Plain moduleany other
.ts
/
.tsx
Free LLM
import { OpenAI } from "https://esm.town/v/std/openai"
→ model
gpt-5-nano
Per-val SQLite
import { sqlite } from "https://esm.town/v/std/sqlite/main.ts"
Blob storage
import { blob } from "https://esm.town/v/std/blob"
Send email
import { email } from "https://esm.town/v/std/email"
NPM import
import x from "npm:package@version"
JSR import
import x from "jsr:@scope/pkg"
Browser-safe
import x from "https://esm.sh/package@version"
Node builtin
import { x } from "node:crypto"
Env var
Deno.env.get("KEY")
(set in val Settings → Env vars)
任务实现方式
创建 val
vt create my-val && cd my-val
克隆 val
vt clone username/valName
实时同步
vt watch
(推荐的开发流程)
单次推送
vt push
在线浏览
vt browse
查看日志
vt tail
HTTP 端点文件名以
.http.tsx
(或
.http.ts
)结尾
定时任务文件名以
.cron.tsx
结尾
邮件处理器文件名以
.email.tsx
结尾
普通模块其他任意
.ts
/
.tsx
文件
免费大语言模型
import { OpenAI } from "https://esm.town/v/std/openai"
→ 使用模型
gpt-5-nano
专属 SQLite
import { sqlite } from "https://esm.town/v/std/sqlite/main.ts"
Blob 存储
import { blob } from "https://esm.town/v/std/blob"
发送邮件
import { email } from "https://esm.town/v/std/email"
NPM 导入
import x from "npm:package@version"
JSR 导入
import x from "jsr:@scope/pkg"
浏览器兼容导入
import x from "https://esm.sh/package@version"
Node 内置模块
import { x } from "node:crypto"
环境变量
Deno.env.get("KEY")
(在 val 设置 → 环境变量中配置)

Multi-File Vals (Preferred)

多文件 Val(推荐方式)

Vals support up to 1000 files. Multi-file is the default and recommended approach — split logic by responsibility.
Vals 最多支持 1000 个文件。多文件是默认且推荐的方式——按职责拆分逻辑。

Trigger detection by filename

基于文件名的触发器检测

The file's name suffix determines its trigger type:
Filename patternTriggerDefault export signature
*.http.tsx
/
*.http.ts
HTTP
(req: Request) => Response | Promise<Response>
*.cron.tsx
/
*.cron.ts
Cron
(interval: Interval) => unknown
*.email.tsx
/
*.email.ts
Email
(email: Email) => unknown
anything else (
.ts
/
.tsx
)
script/moduleregular ESM module
文件的名称后缀决定了其触发器类型:
文件名模式触发器类型默认导出签名
*.http.tsx
/
*.http.ts
HTTP
(req: Request) => Response | Promise<Response>
*.cron.tsx
/
*.cron.ts
定时任务
(interval: Interval) => unknown
*.email.tsx
/
*.email.ts
邮件
(email: Email) => unknown
其他格式(
.ts
/
.tsx
脚本/模块常规 ESM 模块

Recommended layout

推荐目录结构

my-val/
├── deno.json              # Deno config (auto-generated by vt)
├── main.http.tsx          # HTTP entry point
├── daily.cron.tsx         # Scheduled job
├── inbox.email.tsx        # Email handler
├── backend/
│   ├── db.ts              # SQLite queries
│   └── routes.ts          # Hono routes
├── frontend/
│   ├── index.html
│   ├── index.tsx
│   └── style.css
└── shared/
    └── types.ts           # Types used on server + client
my-val/
├── deno.json              # Deno 配置(由 vt 自动生成)
├── main.http.tsx          # HTTP 入口文件
├── daily.cron.tsx         # 定时任务
├── inbox.email.tsx        # 邮件处理器
├── backend/
│   ├── db.ts              # SQLite 查询逻辑
│   └── routes.ts          # Hono 路由
├── frontend/
│   ├── index.html
│   ├── index.tsx
│   └── style.css
└── shared/
    └── types.ts           # 服务端与客户端共用类型

Importing between files

文件间导入

Use relative paths — they survive branching, remixing, and renaming:
ts
import { fetchUser } from "./backend/db.ts";
import type { User } from "./shared/types.ts";
Do not import other files via the
https://esm.town/v/...
URL inside the same val — relative imports are correct.
使用相对路径——它们在分支、 remix 和重命名操作中都能正常工作:
ts
import { fetchUser } from "./backend/db.ts";
import type { User } from "./shared/types.ts";
请勿在同一个 val 内通过
https://esm.town/v/...
URL 导入其他文件——相对路径才是正确的方式。

Serving frontend assets from an HTTP val

从 HTTP val 中提供前端资源

ts
import { readFile, serveFile } from "https://esm.town/v/std/utils/index.ts";
import { Hono } from "npm:hono";

const app = new Hono();

app.get("/frontend/*", c => serveFile(c.req.path, import.meta.url));
app.get("/shared/*",   c => serveFile(c.req.path, import.meta.url));

app.get("/", async c => {
  const html = await readFile("/frontend/index.html", import.meta.url);
  return c.html(html);
});

export default app.fetch;
readFile
/
serveFile
/
listFiles
/
parseProject
are server-only — they don't run in the browser.
ts
import { readFile, serveFile } from "https://esm.town/v/std/utils/index.ts";
import { Hono } from "npm:hono";

const app = new Hono();

app.get("/frontend/*", c => serveFile(c.req.path, import.meta.url));
app.get("/shared/*",   c => serveFile(c.req.path, import.meta.url));

app.get("/", async c => {
  const html = await readFile("/frontend/index.html", import.meta.url);
  return c.html(html);
});

export default app.fetch;
readFile
/
serveFile
/
listFiles
/
parseProject
仅服务端可用的方法——无法在浏览器中运行。

OpenAI for Free

免费使用 OpenAI

Val Town proxies OpenAI for you — no API key required for cheap models.
ts
import { OpenAI } from "https://esm.town/v/std/openai";

const openai = new OpenAI();

const completion = await openai.chat.completions.create({
  model: "gpt-5-nano",            // free tier
  messages: [{ role: "user", content: "say hi" }],
  max_tokens: 100,
});

console.log(completion.choices[0].message.content);
Free models (output < $1 / Mtok):
gpt-5-nano
,
gpt-4.1-nano
,
gpt-4o-mini
. Pro tier: 10 expensive requests / 24h rolling window, then bumped to
gpt-5-nano
. Limits: 10 requests / minute per user. Chat completions endpoint only. Bring your own key: Set
OPENAI_API_KEY
env var and
import { OpenAI } from "npm:openai"
instead.
Val Town 为你代理 OpenAI 服务——低成本模型无需 API 密钥
ts
import { OpenAI } from "https://esm.town/v/std/openai";

const openai = new OpenAI();

const completion = await openai.chat.completions.create({
  model: "gpt-5-nano",            // 免费层级
  messages: [{ role: "user", content: "say hi" }],
  max_tokens: 100,
});

console.log(completion.choices[0].message.content);
免费模型(输出成本 < $1 / 百万 tokens):
gpt-5-nano
,
gpt-4.1-nano
,
gpt-4o-mini
专业层级: 每24小时可使用10次高成本请求,之后自动降级为
gpt-5-nano
限制: 每个用户每分钟最多10次请求。仅支持聊天补全接口。 使用自有密钥: 设置
OPENAI_API_KEY
环境变量,并改为
import { OpenAI } from "npm:openai"

SQLite (Val-Scoped)

SQLite(Val 专属)

Every val ships with a private SQLite database, powered by Turso/libSQL. 10 MB free, 1 GB on Pro.
ts
import { sqlite } from "https://esm.town/v/std/sqlite/main.ts";

await sqlite.execute(`
  create table if not exists notes (
    id integer primary key autoincrement,
    body text not null,
    created_at text default current_timestamp
  )
`);

await sqlite.execute({
  sql: `insert into notes (body) values (?)`,
  args: ["hello"],
});

const result = await sqlite.execute(`select * from notes order by id desc limit 10`);
console.log(result.rows);
Critical:
  • Import path matters:
    sqlite/main.ts
    = val-scoped (preferred). Bare
    sqlite
    = legacy account-scoped global DB.
  • batch()
    exists for transactions:
    sqlite.batch(["sql1", "sql2"])
    .
  • Limited
    ALTER TABLE
    support.
    When changing schema, create a new table (
    users_2
    ,
    users_3
    ) and migrate data rather than altering in place.
  • Always run
    create table if not exists
    before queries.
  • ORMs work: Drizzle and others over libSQL — see
    https://docs.val.town/reference/std/sqlite/orms
    .
每个 val 都配有私有 SQLite 数据库,由 Turso/libSQL 提供支持。免费版10 MB,专业版1 GB。
ts
import { sqlite } from "https://esm.town/v/std/sqlite/main.ts";

await sqlite.execute(`
  create table if not exists notes (
    id integer primary key autoincrement,
    body text not null,
    created_at text default current_timestamp
  )
`);

await sqlite.execute({
  sql: `insert into notes (body) values (?)`,
  args: ["hello"],
});

const result = await sqlite.execute(`select * from notes order by id desc limit 10`);
console.log(result.rows);
关键注意事项:
  • 导入路径很重要:
    sqlite/main.ts
    = Val 专属(推荐)。直接
    sqlite
    = 旧版账户级全局数据库。
  • 支持事务
    batch()
    sqlite.batch(["sql1", "sql2"])
  • ALTER TABLE
    支持有限
    。修改表结构时,应创建新表(如
    users_2
    ,
    users_3
    )并迁移数据,而非直接修改原表。
  • 查询前始终执行
    create table if not exists
  • 支持 ORM:Drizzle 等基于 libSQL 的 ORM——详见
    https://docs.val.town/reference/std/sqlite/orms

Blob Storage

Blob 存储

Account-scoped key-value store on Cloudflare R2. Use it for binary data (images, PDFs) since vals can't write the filesystem.
ts
import { blob } from "https://esm.town/v/std/blob";

await blob.setJSON("config", { theme: "dark" });
const config = await blob.getJSON("config");
const keys = await blob.list("user_");        // prefix list
await blob.delete("config");
Don't use Deno KV — it isn't supported for storage.
基于 Cloudflare R2 的账户级键值存储。用于存储二进制数据(图片、PDF),因为 vals 无法写入文件系统。
ts
import { blob } from "https://esm.town/v/std/blob";

await blob.setJSON("config", { theme: "dark" });
const config = await blob.getJSON("config");
const keys = await blob.list("user_");        // 按前缀列出
await blob.delete("config");
请勿使用 Deno KV——该存储方式不受支持。

Importing Anything via CDN

通过 CDN 导入任意资源

Val Town runs on Deno, so HTTP imports are first-class. Pin versions to avoid surprise breaks.
SourceSyntax
NPM
import { z } from "npm:zod@3.23.8"
JSR
import { concat } from "jsr:@std/bytes@1.0.2"
esm.sh
import React from "https://esm.sh/react@18.2.0"
deno.land/x
import { Hono } from "https://deno.land/x/hono@v4.5.0/mod.ts"
Node builtin
import { createHmac } from "node:crypto"
Other val
import { x } from "https://esm.town/v/user/valName"
When to prefer
esm.sh
over
npm:
:
code that runs in the browser, or NPM modules that try to touch the filesystem (sandbox blocks fs access — esm.sh often patches around it).
React pinning: ALL React + ReactDOM versions must match. Use
?deps=react@18.2.0,react-dom@18.2.0
and put
/** @jsxImportSource https://esm.sh/react@18.2.0 */
at the top of any TSX file.
ts
/** @jsxImportSource https://esm.sh/react@18.2.0 */
import { useState } from "https://esm.sh/react@18.2.0?deps=react@18.2.0";
Val Town 运行于 Deno,因此HTTP 导入是一等公民。固定版本以避免意外故障。
来源语法
NPM
import { z } from "npm:zod@3.23.8"
JSR
import { concat } from "jsr:@std/bytes@1.0.2"
esm.sh
import React from "https://esm.sh/react@18.2.0"
deno.land/x
import { Hono } from "https://deno.land/x/hono@v4.5.0/mod.ts"
Node 内置模块
import { createHmac } from "node:crypto"
其他 val
import { x } from "https://esm.town/v/user/valName"
何时优先使用
esm.sh
而非
npm:
运行在浏览器中的代码,或尝试访问文件系统的 NPM 模块(沙箱会阻止文件系统访问——esm.sh 通常会对此进行补丁处理)。
React 版本固定: 所有 React + ReactDOM 版本必须一致。使用
?deps=react@18.2.0,react-dom@18.2.0
,并在任何 TSX 文件顶部添加
/** @jsxImportSource https://esm.sh/react@18.2.0 */
ts
/** @jsxImportSource https://esm.sh/react@18.2.0 */
import { useState } from "https://esm.sh/react@18.2.0?deps=react@18.2.0";

Cron Jobs

定时任务(Cron Jobs)

Name a file
*.cron.tsx
and the platform schedules it. Free tier: every 15 min minimum. Pro: every 1 min.
ts
// daily.cron.tsx
export default async function (interval: Interval) {
  console.log("last ran at:", interval.lastRunAt);
  // do work
}
The schedule itself (cron expression or simple interval) is configured in the val editor UI (
+
button → CRON), not in code. Crons run in UTC — adjust expressions for local time.
Common expressions:
ExpressionMeaning
0 * * * *
every hour on the hour
*/15 * * * *
every 15 min (free-tier minimum)
0 9 * * *
09:00 UTC daily
0 0 * * 1
Mondays at 00:00 UTC
interval.lastRunAt
is
Date | undefined
— undefined on the first run. Use it to fetch only items newer than the last run.
将文件命名为
*.cron.tsx
,平台会自动调度执行。免费版最小间隔为15分钟;专业版最小间隔为1分钟。
ts
// daily.cron.tsx
export default async function (interval: Interval) {
  console.log("上次运行时间:", interval.lastRunAt);
  // 执行任务
}
调度规则(Cron 表达式或简单间隔)在Val 编辑器 UI中配置(点击
+
按钮 → CRON),而非在代码中设置。定时任务以UTC 时间运行——需根据本地时间调整表达式。
常用表达式:
表达式含义
0 * * * *
每小时整点运行
*/15 * * * *
每15分钟运行一次(免费版最小间隔)
0 9 * * *
每天 UTC 时间09:00运行
0 0 * * 1
每周一 UTC 时间00:00运行
interval.lastRunAt
类型为
Date | undefined
——首次运行时为 undefined。可用于仅获取上次运行后新增的内容。

Sending Email

发送邮件

ts
import { email } from "https://esm.town/v/std/email";

await email({
  subject: "deploy succeeded",
  text: "everything green",
  html: "<h1>everything green</h1>",
});
By default the message goes to the val owner. The free tier allows email out; receiving requires an
*.email.tsx
handler on a paid plan.
ts
import { email } from "https://esm.town/v/std/email";

await email({
  subject: "部署成功",
  text: "一切正常",
  html: "<h1>一切正常</h1>",
});
默认情况下,邮件发送给 val 的所有者。免费版仅支持发送邮件;接收邮件需要付费计划并配置
*.email.tsx
处理器。

Deno + TypeScript Best Practices

Deno + TypeScript 最佳实践

deno.json

deno.json

vt
generates this for you. Keep
valtown.d.ts
in
types
so editor sees
Interval
,
Email
, etc.
json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": false,
    "types": ["https://www.val.town/types/valtown.d.ts"],
    "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns", "deno.unstable"]
  },
  "node_modules_dir": false,
  "experimental": {
    "unstable-temporal": true,
    "unstable-sloppy-imports": true
  }
}
vt
会自动生成该文件。将
valtown.d.ts
保留在
types
中,以便编辑器识别
Interval
Email
等类型。
json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": false,
    "types": ["https://www.val.town/types/valtown.d.ts"],
    "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns", "deno.unstable"]
  },
  "node_modules_dir": false,
  "experimental": {
    "unstable-temporal": true,
    "unstable-sloppy-imports": true
  }
}

Runtime constraints

运行时限制

  • No filesystem writes. No
    Deno.writeFile
    , no
    fs
    module. Use blob storage or SQLite.
  • No binary files in the val itself — only text. Store binaries in blob.
  • --allow-net
    is on;
    fetch
    works for any URL.
  • Experimental Temporal API is available.
  • No
    alert()
    /
    prompt()
    /
    confirm()
    (no UI process).
  • No Deno KV.
  • Response.redirect()
    is broken — return
    new Response(null, { status: 302, headers: { Location: "/x" } })
    instead.
  • 无法写入文件系统。不支持
    Deno.writeFile
    ,也不支持
    fs
    模块。使用 Blob 存储或 SQLite。
  • Val 本身仅支持文本文件——二进制文件需存储在 Blob 中。
  • 已开启
    --allow-net
    fetch
    可访问任意 URL。
  • 支持实验性 Temporal API。
  • 不支持
    alert()
    /
    prompt()
    /
    confirm()
    (无 UI 进程)。
  • 不支持 Deno KV。
  • Response.redirect()
    存在问题——请返回
    new Response(null, { status: 302, headers: { Location: "/x" } })
    替代。

Code style

代码风格

  • Prefer functional ES2020+ —
    const
    , arrow fns, destructuring.
  • Add explicit types for exported functions and shared interfaces.
  • Use
    try/catch
    only when you can resolve the error locally; otherwise let it bubble for full stack traces.
  • For Hono apps add an unwrapping error handler so you see real stack traces:
    ts
    app.onError((_err, _c) => { throw _err; });
  • Code in
    shared/
    runs in both server and browser — never reference
    Deno
    there. Use
    https://esm.sh
    imports if it must work in both.
  • Secrets only via
    Deno.env.get()
    — never bake keys into source.
  • 优先使用函数式 ES2020+ 语法——
    const
    、箭头函数、解构赋值。
  • 为导出的函数和共享接口添加显式类型。
  • 仅当能在本地处理错误时使用
    try/catch
    ;否则让错误冒泡以获取完整堆栈跟踪。
  • 对于 Hono 应用,添加错误解包处理器以查看真实堆栈跟踪:
    ts
    app.onError((_err, _c) => { throw _err; });
  • shared/
    目录下的代码会在服务端和浏览器中运行——切勿在此引用
    Deno
    。如需兼容两端,使用
    https://esm.sh
    导入。
  • 仅通过
    Deno.env.get()
    获取密钥——切勿将密钥硬编码到源码中。

Debugging client-side

客户端调试

Drop this into your HTML head to surface client-side errors back to val logs:
html
<script src="https://esm.town/v/std/catch"></script>
将以下代码添加到 HTML 的 head 中,可将客户端错误反馈到 val 日志:
html
<script src="https://esm.town/v/std/catch"></script>

CLI: vt

CLI:vt

vt create <name> [dir]   # new val (--private, --unlisted, --org-name)
vt clone user/val [dir]  # clone existing
vt watch                 # auto-sync local edits to prod (best dev loop)
vt push                  # one-shot upload
vt pull                  # download remote changes
vt status                # working-tree diff
vt branch / checkout     # branch ops
vt tail [user/val]       # stream live logs
vt browse                # open val in browser
vt list [-a]             # list your vals
vt remix <user/val>      # fork
vt config set <k> <v>    # config (run `vt config options` to list)
vt watch
is the recommended workflow: edit in your editor, changes deploy in <1s, logs stream with
vt tail
in another pane.
vt create <name> [dir]   # 创建新 val(可选参数:--private, --unlisted, --org-name)
vt clone user/val [dir]  # 克隆现有 val
vt watch                 # 自动同步本地修改到生产环境(最佳开发流程)
vt push                  # 单次上传
vt pull                  # 下载远程更改
vt status                # 工作区差异
vt branch / checkout     # 分支操作
vt tail [user/val]       # 实时流式查看日志
vt browse                # 在浏览器中打开 val
vt list [-a]             # 列出你的 vals
vt remix <user/val>      # 复刻(fork)
vt config set <k> <v>    # 配置(运行 `vt config options` 查看可选配置)
vt watch
是推荐的工作流:在编辑器中编辑,更改会在1秒内部署到生产环境,同时在另一个终端窗口使用
vt tail
查看日志流。

Common Mistakes

常见错误

MistakeFix
Trigger file not detectedFilename must match
*.http.tsx
/
*.cron.tsx
/
*.email.tsx
exactly
Response.redirect()
returns blank
Use
new Response(null, { status: 302, headers: { Location: "/x" } })
React renders nothing / hooks errorAll React deps must pin same version; add
?deps=react@18.2.0,react-dom@18.2.0
ALTER TABLE
fails
Create new table with version suffix and migrate
parseProject
returns nothing in browser
It's server-only — call from server, inject into HTML
NPM module crashes in sandboxModule probably touched filesystem; switch to
https://esm.sh/...
Cron fires more often than expectedYou set local-time expression; crons are UTC
Importing same-val file via
esm.town
URL
Use relative
./path.ts
instead
Storing binary in a val fileVals are text-only; use
blob.setJSON
(or raw blob API for bytes)
Deno.env.set()
doesn't persist
Env vars are read-only at runtime; set in val Settings UI
错误修复方案
触发器文件未被检测到文件名必须严格匹配
*.http.tsx
/
*.cron.tsx
/
*.email.tsx
Response.redirect()
返回空白
使用
new Response(null, { status: 302, headers: { Location: "/x" } })
替代
React 无法渲染 / Hooks 报错所有 React 依赖必须固定相同版本;添加
?deps=react@18.2.0,react-dom@18.2.0
ALTER TABLE
执行失败
创建带版本后缀的新表并迁移数据
parseProject
在浏览器中返回空值
该方法仅服务端可用——从服务端调用并注入到 HTML 中
NPM 模块在沙箱中崩溃模块可能尝试访问文件系统;切换为
https://esm.sh/...
导入
定时任务执行频率超出预期你设置了本地时间表达式;定时任务以 UTC 时间运行
通过
esm.town
URL 导入同一 val 内的文件
使用相对路径
./path.ts
替代
将二进制文件存储在 val 文件中Vals 仅支持文本;使用
blob.setJSON
(或针对字节的原生 Blob API)
Deno.env.set()
无法持久化
运行时环境变量为只读;在 val 设置 UI 中配置

When NOT to Use Val Town

不适合使用 Val Town 的场景

  • Long-running compute (> request timeout) → use a proper VM or Cloud Run.
  • Heavy filesystem work → no fs available.
  • Large binary processing → 10 MB SQLite cap on free tier; blob is fine for storage but not for in-memory crunching.
  • Workloads needing custom Deno permissions → can't change the sandbox.
  • 长时间运行的计算(超过请求超时时间)→ 使用专用虚拟机或 Cloud Run。
  • 大量文件系统操作 → 无文件系统访问权限。
  • 大型二进制处理 → 免费版 SQLite 容量限制为10 MB;Blob 适合存储但不适合内存处理。
  • 需要自定义 Deno 权限的工作负载 → 无法修改沙箱配置。

Reference Links

参考链接