val-town
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVal 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 , watch syncs to production.
vtVal Town 是一个运行 TypeScript/TSX 的无服务器 Deno 平台。val 是一个带版本控制的文件目录(默认支持多文件),可以暴露 HTTP、定时任务(Cron)或邮件触发器。每个 val 都配有私有 SQLite 数据库、Blob 存储,并且可免费访问低成本的 OpenAI 模型。
核心思维模型: Deno + 多文件项目 + 基于后缀的触发器检测。使用 在本地编辑,实时同步到生产环境。
vtQuick Reference
快速参考
| Task | Approach |
|---|---|
| Create val | |
| Clone val | |
| Live sync | |
| One-shot push | |
| Browse online | |
| Tail logs | |
| HTTP endpoint | filename ends in |
| Cron job | filename ends in |
| Email handler | filename ends in |
| Plain module | any other |
| Free LLM | |
| Per-val SQLite | |
| Blob storage | |
| Send email | |
| NPM import | |
| JSR import | |
| Browser-safe | |
| Node builtin | |
| Env var | |
| 任务 | 实现方式 |
|---|---|
| 创建 val | |
| 克隆 val | |
| 实时同步 | |
| 单次推送 | |
| 在线浏览 | |
| 查看日志 | |
| HTTP 端点 | 文件名以 |
| 定时任务 | 文件名以 |
| 邮件处理器 | 文件名以 |
| 普通模块 | 其他任意 |
| 免费大语言模型 | |
| 专属 SQLite | |
| Blob 存储 | |
| 发送邮件 | |
| NPM 导入 | |
| JSR 导入 | |
| 浏览器兼容导入 | |
| Node 内置模块 | |
| 环境变量 | |
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 pattern | Trigger | Default export signature |
|---|---|---|
| HTTP | |
| Cron | |
| | |
anything else ( | script/module | regular ESM module |
文件的名称后缀决定了其触发器类型:
| 文件名模式 | 触发器类型 | 默认导出签名 |
|---|---|---|
| HTTP | |
| 定时任务 | |
| 邮件 | |
其他格式( | 脚本/模块 | 常规 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 + clientmy-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 URL inside the same val — relative imports are correct.
https://esm.town/v/...使用相对路径——它们在分支、 remix 和重命名操作中都能正常工作:
ts
import { fetchUser } from "./backend/db.ts";
import type { User } from "./shared/types.ts";请勿在同一个 val 内通过 URL 导入其他文件——相对路径才是正确的方式。
https://esm.town/v/...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;readFileserveFilelistFilesparseProjectts
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;readFileserveFilelistFilesparseProjectOpenAI 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): , , .
Pro tier: 10 expensive requests / 24h rolling window, then bumped to .
Limits: 10 requests / minute per user. Chat completions endpoint only.
Bring your own key: Set env var and instead.
gpt-5-nanogpt-4.1-nanogpt-4o-minigpt-5-nanoOPENAI_API_KEYimport { OpenAI } from "npm:openai"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):, , 。
专业层级: 每24小时可使用10次高成本请求,之后自动降级为 。
限制: 每个用户每分钟最多10次请求。仅支持聊天补全接口。
使用自有密钥: 设置 环境变量,并改为 。
gpt-5-nanogpt-4.1-nanogpt-4o-minigpt-5-nanoOPENAI_API_KEYimport { 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: = val-scoped (preferred). Bare
sqlite/main.ts= legacy account-scoped global DB.sqlite - exists for transactions:
batch().sqlite.batch(["sql1", "sql2"]) - Limited support. When changing schema, create a new table (
ALTER TABLE,users_2) and migrate data rather than altering in place.users_3 - Always run before queries.
create table if not exists - 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);关键注意事项:
- 导入路径很重要:= Val 专属(推荐)。直接
sqlite/main.ts= 旧版账户级全局数据库。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.
| Source | Syntax |
|---|---|
| NPM | |
| JSR | |
| esm.sh | |
| deno.land/x | |
| Node builtin | |
| Other val | |
When to prefer over : 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).
esm.shnpm:React pinning: ALL React + ReactDOM versions must match. Use and put at the top of any TSX file.
?deps=react@18.2.0,react-dom@18.2.0/** @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";Val Town 运行于 Deno,因此HTTP 导入是一等公民。固定版本以避免意外故障。
| 来源 | 语法 |
|---|---|
| NPM | |
| JSR | |
| esm.sh | |
| deno.land/x | |
| Node 内置模块 | |
| 其他 val | |
何时优先使用 而非 : 运行在浏览器中的代码,或尝试访问文件系统的 NPM 模块(沙箱会阻止文件系统访问——esm.sh 通常会对此进行补丁处理)。
esm.shnpm:React 版本固定: 所有 React + ReactDOM 版本必须一致。使用 ,并在任何 TSX 文件顶部添加 。
?deps=react@18.2.0,react-dom@18.2.0/** @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 and the platform schedules it. Free tier: every 15 min minimum. Pro: every 1 min.
*.cron.tsxts
// 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:
| Expression | Meaning |
|---|---|
| every hour on the hour |
| every 15 min (free-tier minimum) |
| 09:00 UTC daily |
| Mondays at 00:00 UTC |
interval.lastRunAtDate | undefined将文件命名为 ,平台会自动调度执行。免费版最小间隔为15分钟;专业版最小间隔为1分钟。
*.cron.tsxts
// daily.cron.tsx
export default async function (interval: Interval) {
console.log("上次运行时间:", interval.lastRunAt);
// 执行任务
}调度规则(Cron 表达式或简单间隔)在Val 编辑器 UI中配置(点击按钮 → CRON),而非在代码中设置。定时任务以UTC 时间运行——需根据本地时间调整表达式。
+常用表达式:
| 表达式 | 含义 |
|---|---|
| 每小时整点运行 |
| 每15分钟运行一次(免费版最小间隔) |
| 每天 UTC 时间09:00运行 |
| 每周一 UTC 时间00:00运行 |
interval.lastRunAtDate | undefinedSending 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 handler on a paid plan.
*.email.tsxts
import { email } from "https://esm.town/v/std/email";
await email({
subject: "部署成功",
text: "一切正常",
html: "<h1>一切正常</h1>",
});默认情况下,邮件发送给 val 的所有者。免费版仅支持发送邮件;接收邮件需要付费计划并配置 处理器。
*.email.tsxDeno + TypeScript Best Practices
Deno + TypeScript 最佳实践
deno.json
deno.json
vtvaltown.d.tstypesIntervalEmailjson
{
"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
}
}vtvaltown.d.tstypesIntervalEmailjson
{
"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 , no
Deno.writeFilemodule. Use blob storage or SQLite.fs - No binary files in the val itself — only text. Store binaries in blob.
- is on;
--allow-networks for any URL.fetch - Experimental Temporal API is available.
- No /
alert()/prompt()(no UI process).confirm() - No Deno KV.
- is broken — return
Response.redirect()instead.new Response(null, { status: 302, headers: { Location: "/x" } })
- 无法写入文件系统。不支持 ,也不支持
Deno.writeFile模块。使用 Blob 存储或 SQLite。fs - Val 本身仅支持文本文件——二进制文件需存储在 Blob 中。
- 已开启 ;
--allow-net可访问任意 URL。fetch - 支持实验性 Temporal API。
- 不支持 /
alert()/prompt()(无 UI 进程)。confirm() - 不支持 Deno KV。
- 存在问题——请返回
Response.redirect()替代。new Response(null, { status: 302, headers: { Location: "/x" } })
Code style
代码风格
- Prefer functional ES2020+ — , arrow fns, destructuring.
const - Add explicit types for exported functions and shared interfaces.
- Use only when you can resolve the error locally; otherwise let it bubble for full stack traces.
try/catch - For Hono apps add an unwrapping error handler so you see real stack traces:
ts
app.onError((_err, _c) => { throw _err; }); - Code in runs in both server and browser — never reference
shared/there. UseDenoimports if it must work in both.https://esm.sh - Secrets only via — never bake keys into source.
Deno.env.get()
- 优先使用函数式 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 watchvt tailvt 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 watchvt tailCommon Mistakes
常见错误
| Mistake | Fix |
|---|---|
| Trigger file not detected | Filename must match |
| Use |
| React renders nothing / hooks error | All React deps must pin same version; add |
| Create new table with version suffix and migrate |
| It's server-only — call from server, inject into HTML |
| NPM module crashes in sandbox | Module probably touched filesystem; switch to |
| Cron fires more often than expected | You set local-time expression; crons are UTC |
Importing same-val file via | Use relative |
| Storing binary in a val file | Vals are text-only; use |
| Env vars are read-only at runtime; set in val Settings UI |
| 错误 | 修复方案 |
|---|---|
| 触发器文件未被检测到 | 文件名必须严格匹配 |
| 使用 |
| React 无法渲染 / Hooks 报错 | 所有 React 依赖必须固定相同版本;添加 |
| 创建带版本后缀的新表并迁移数据 |
| 该方法仅服务端可用——从服务端调用并注入到 HTML 中 |
| NPM 模块在沙箱中崩溃 | 模块可能尝试访问文件系统;切换为 |
| 定时任务执行频率超出预期 | 你设置了本地时间表达式;定时任务以 UTC 时间运行 |
通过 | 使用相对路径 |
| 将二进制文件存储在 val 文件中 | Vals 仅支持文本;使用 |
| 运行时环境变量为只读;在 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
参考链接
- Docs index: https://docs.val.town/llms.txt
- OpenAI proxy: https://docs.val.town/reference/std/openai.md
- SQLite usage: https://docs.val.town/reference/std/sqlite/usage.md
- Importing: https://docs.val.town/reference/import.md
- Runtime: https://docs.val.town/reference/runtime.md
- Cron: https://docs.val.town/vals/cron.md
- HTTP: https://docs.val.town/vals/http.md
- Limitations: https://docs.val.town/vals/limitations.md
- 文档索引:https://docs.val.town/llms.txt
- OpenAI 代理:https://docs.val.town/reference/std/openai.md
- SQLite 使用:https://docs.val.town/reference/std/sqlite/usage.md
- 导入指南:https://docs.val.town/reference/import.md
- 运行时说明:https://docs.val.town/reference/runtime.md
- 定时任务:https://docs.val.town/vals/cron.md
- HTTP 服务:https://docs.val.town/vals/http.md
- 限制说明:https://docs.val.town/vals/limitations.md