# Deno Ecosystem Field Guide
Source: /en/docs/
Deno is a JavaScript, TypeScript, and WebAssembly runtime. It runs TypeScript directly, denies file, network, environment, and subprocess access by default, and ships dependency management, formatting, linting, testing, documentation, and compilation in one CLI.
This site does not mirror the [official Deno documentation](https://docs.deno.com). It organizes migration routes, framework decisions, production boundaries, and project blueprints for working developers.
## Choose your task
## Capability map
| Goal | First command | Guide |
| --- | --- | --- |
| Run TypeScript | `deno run main.ts` | [Runtime](/en/docs/core/runtime) |
| Restrict system access | `--allow-*` / `--deny-*` | [Permissions](/en/docs/core/permissions) |
| Add JSR or npm packages | `deno install ` | [Dependencies](/en/docs/core/dependencies) |
| Format, lint, and type-check | `deno fmt && deno lint && deno check` | [Built-in tools](/en/docs/core/tooling) |
| Run tests | `deno test` | [Testing](/en/docs/core/testing) |
| Reproduce installs and audit | `deno ci && deno audit` | [Supply chain](/en/docs/core/supply-chain) |
| Standardize project commands | `deno task ` | [CLI and configuration](/en/docs/reference/cli-and-config) |
| Build full-stack pages | Fresh 2 | [Fresh guide](/en/docs/web/fresh) |
| Build a cross-runtime API | Hono | [Hono guide](/en/docs/web/hono) |
| Connect PostgreSQL | postgres.js / Drizzle | [Databases](/en/docs/database/postgres-drizzle) |
| Expose AI tools | MCP TypeScript SDK | [MCP server](/en/docs/ai/mcp-server) |
| Ship a single-file binary | `deno compile` | [Compile & distribute](/en/docs/core/compile) |
| Set up a CI pipeline | `setup-deno` + `deno ci` | [CI/CD in practice](/en/docs/deploy/ci-cd) |
| Manage environment variables | `--env-file` / `--allow-env` | [Environment variables](/en/docs/core/env-variables) |
| Choose among three runtimes | criteria comparison | [Deno vs Node vs Bun](/en/docs/reference/deno-vs-node-bun) |
These docs use Deno 2.x as the stable baseline. For minor-version, preview API, or hosted-platform behavior, run `deno --version` and verify the matching official page and release notes.
Append `.md` to any page URL for Markdown. The site also exposes [`/en/llms.txt`](/en/llms.txt) and [`/en/llms-full.txt`](/en/llms-full.txt).
---
# About this site
Source: /en/docs/about/
This is a community-maintained, Chinese-first Deno ecosystem field guide with complete English coverage. It is not affiliated with Deno Land Inc. and does not replace the [official Deno documentation](https://docs.deno.com). It focuses on migration order, framework selection, project structure, production boundaries, and recurring developer problems beyond an API reference.
## Fact sources
The baseline review used [`denoland/docs`](https://github.com/denoland/docs) commit [`e0241c6`](https://github.com/denoland/docs/commit/e0241c6ffd7fffafcf707e898a038be5685e5a97), with dated re-checks against primary Fresh, Hono, Oak, MCP, OpenAI, and hosting documentation. Runtime API truth comes from official docs, Deno CLI help, and [`denoland/deno`](https://github.com/denoland/deno); frameworks and platforms use their own official docs.
## Editorial policy
- Do not mirror the complete API reference; maintain task paths, decision boundaries, and verification methods.
- Mark volatile behavior with a version, date, or “verify before release.”
- Use least privilege in examples; never hide required grants with `-A`.
- Keep Chinese and English slugs and semantics aligned while writing naturally in each language.
- Use primary sources for security, cloud platform, and AI SDK claims.
- Record fact review dates in `lastVerified` frontmatter.
## Machine-readable access
Append `.md` to a page, use `/en/llms.txt` for the index, or `/en/llms-full.txt` for the corpus. These endpoints are marked `noindex` so they do not compete with HTML pages.
When reporting an error, include Deno version, page URL, primary source, and a reproducible command.
---
# AI / Agent entry point
Source: /en/docs/ai/
## Reading order
1. Read [`/en/llms.txt`](/en/llms.txt) for the page index.
2. Load one task page as Markdown, such as [`/en/docs/core/permissions.md`](/en/docs/core/permissions.md).
3. Use [`/en/llms-full.txt`](/en/llms-full.txt) only for cross-cutting work.
4. Before editing, inspect `deno.json(c)`, `deno.lock`, `package.json`, imports, tasks, and CI.
5. After editing, run `fmt --check`, `lint`, `check`, and relevant tests, then report exact commands.
## Task routing
| Goal | Read first | Then |
| --- | --- | --- |
| Run/debug TypeScript | [Runtime](/en/docs/core/runtime) | [Permissions](/en/docs/core/permissions) |
| Install a dependency | [Dependencies](/en/docs/core/dependencies) | [CLI and config](/en/docs/reference/cli-and-config) |
| Change a monorepo | [Workspaces](/en/docs/core/workspaces) | [Production baseline](/en/docs/deploy/production-baseline) |
| Add tests | [Testing](/en/docs/core/testing) | [Built-in tools](/en/docs/core/tooling) |
| Migrate Node | [Node migration](/en/docs/migration/from-node) | [Common errors](/en/docs/reference/errors) |
| Build an AI service | [AI apps](/en/docs/ai/building-ai-apps) | [Permissions](/en/docs/core/permissions) |
| Call OpenAI | [Deno + OpenAI](/en/docs/ai/openai) | [AI apps](/en/docs/ai/building-ai-apps) |
| Build MCP tools | [MCP server](/en/docs/ai/mcp-server) | [Agent architecture](/en/docs/ai/agent-loop) |
| Design an agent loop | [Agent architecture](/en/docs/ai/agent-loop) | [Agent rules](/en/docs/ai/agent-rules) |
| Run untrusted agent code | [Deno Sandbox](/en/docs/ai/sandbox) | [Permissions](/en/docs/core/permissions) |
| Release a service | [Deployment decisions](/en/docs/deploy) | target platform's official docs |
## Invariants
```md
- Do not replace the repository's package or task conventions before reading them.
- Do not grant -A to silence a permission error; identify the exact resource.
- Running TypeScript does not replace `deno check` in CI.
- Keep deno.lock unless the user explicitly approves dependency resolution changes.
- Verify current Deno, framework, and platform behavior from primary sources.
- Never print environment values or pass untrusted text through a shell command.
```
Deno also publishes [deno.com/agents.md](https://deno.com/agents.md) and [denoland/skills](https://github.com/denoland/skills). This site's rules focus on task routing and do not replace those official skills.
---
# Deno agent architecture
Source: /en/docs/ai/agent-loop/
An agent is not a special runtime. It is a loop of model decision, tool execution, result return, and stop evaluation. Deno's value is explicit permission around tools plus composable Web APIs, npm SDKs, MCP, and Sandbox boundaries.
## Choose the minimum complexity
| Need | Starting point |
| --- | --- |
| one model and a few function tools | vendor SDK and a short explicit loop |
| tools shared by several clients | MCP server |
| multi-provider graphs, tracing, integrations | evaluate LangChain.js / LangGraph |
| document indexing, RAG, data connectors | evaluate LlamaIndex.TS |
| model-generated code execution | Deno Sandbox, not plain `Deno.Command` |
Installing a framework through npm compatibility does not prove every integration works on Deno. Add smoke tests for the actual loader, vector store, native dependency, and streaming path before selection.
## Boundaries of a controlled loop
```text
user request
→ model (only approved tool schemas)
→ validate tool name and arguments
→ authorize or request human approval
→ run a time-bounded tool
→ return structured results
→ stop condition or maximum steps
```
- Set `maxSteps`, a total deadline, token or cost budget, and retry count.
- Tool handlers must not accept arbitrary shell, SQL, URL, or file paths.
- Separate read and write authority; deletion, payment, publishing, and production changes need explicit approval.
- Prompt injection can arrive through pages, databases, and MCP resources. Retrieved content is neither instruction nor authorization.
- Record tool name, argument summary, latency, result type, and error without logging secrets.
Continue with [OpenAI](/en/docs/ai/openai), [MCP server](/en/docs/ai/mcp-server), and [Deno Sandbox](/en/docs/ai/sandbox).
---
# Paste-ready agent rules
Source: /en/docs/ai/agent-rules/
Adapt this block to the repository before adding it to `AGENTS.md`:
```md
## Deno workflow
- Read deno.json/deno.jsonc, deno.lock, package.json, and CI before changing commands.
- Reuse existing imports and tasks. Do not invent configuration keys.
- Add packages with the repository's Deno CLI workflow; keep one reviewed lockfile diff.
- Grant only scoped --allow-* permissions. Never use -A merely to make a command pass.
- Treat --allow-run and --allow-ffi as sandbox escape boundaries.
- After changes run: deno fmt --check, deno lint, deno check, and relevant deno test targets.
- Report exact commands and failures; do not claim tests you did not run.
- Ask before deleting lockfiles, changing major versions, publishing, or deploying.
```
## Repository facts to add
- pinned Deno version and upgrade process;
- authoritative config and workspace root;
- existing dev, test, check, build, and deploy task names;
- allowed hosts, paths, and environment variable names;
- whether `package.json` / `node_modules` compatibility is used;
- production platform and rollback workflow.
Instructions do not enforce execution boundaries. CI, containers, and the deployment platform still need least privilege and approvals.
---
# Build AI applications with Deno
Source: /en/docs/ai/building-ai-apps/
## Minimal security boundary
An AI service commonly listens, reads one provider key, and contacts one API:
```bash
deno run \
--allow-net=0.0.0.0:8000,api.example.com:443 \
--allow-env=MODEL_API_KEY \
--no-prompt main.ts
```
Model names, API paths, and SDK methods change quickly. Query the provider's current official docs before implementation; never guess a default model in shared code.
## Streaming proxy
```ts
Deno.serve(async (request) => {
const upstream = await fetch("https://api.example.com/v1/responses", {
method: "POST",
headers: {
authorization: `Bearer ${Deno.env.get("MODEL_API_KEY")}`,
"content-type": "application/json",
},
body: await request.text(),
signal: request.signal,
});
return new Response(upstream.body, {
status: upstream.status,
headers: { "content-type": upstream.headers.get("content-type") ?? "text/event-stream" },
});
});
```
Production code must also limit body size, validate schema, set a timeout, map errors, and cancel upstream work when the client disconnects.
## Tool calls
- Map tool names to fixed functions; never interpolate model text into a shell.
- Validate parameters with a schema and show impact before authorization.
- Restrict file tools to a workspace and network tools to a host allowlist.
- Separate reads from writes; require humans for deletion, publishing, payment, and production changes.
- Log tool name, duration, result class, and request ID, but not secrets or full sensitive inputs.
## RAG and evaluation
Keep source URL, version, update time, and access labels on chunks. Retrieved context is not authorization. Maintain a fixed evaluation set for refusal, prompt injection, timeouts, provider 429/5xx responses, and data leakage.
Official entry points: [Deno AI](https://docs.deno.com/ai/) and the [Deno LLM tutorial](https://docs.deno.com/examples/llm_tutorial/).
---
# Build an MCP server with Deno
Source: /en/docs/ai/mcp-server/
An MCP server exposes tools, resources, and prompts to AI clients through a protocol. Deno is a natural fit for a local stdio server: single-file TypeScript, explicit permissions, and no separate compile step.
```bash
deno add npm:@modelcontextprotocol/sdk npm:zod
```
```ts title="mcp_server.ts"
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({ name: "project-info", version: "1.0.0" });
server.registerTool(
"read_project_note",
{
description: "Read one approved note by its short name",
inputSchema: { name: z.string().regex(/^[a-z0-9-]+$/) },
},
async ({ name }) => ({
content: [{
type: "text",
text: await Deno.readTextFile(`./notes/${name}.md`),
}],
}),
);
await server.connect(new StdioServerTransport());
```
Grant the client read access only to the notes directory:
```json
{
"mcpServers": {
"project-info": {
"command": "deno",
"args": ["run", "--allow-read=./notes", "mcp_server.ts"]
}
}
}
```
Do not print debug logs to stdout; that corrupts JSON-RPC. Log to stderr and keep secrets out of tool arguments and logs.
Prefer Streamable HTTP for remote servers. The old HTTP+SSE transport exists only for compatibility. A public deployment also needs authentication, Origin and DNS-rebinding defenses, rate limits, and authorization on every tool call.
Official references: [Deno MCP server example](https://docs.deno.com/examples/mcp_server/), [MCP TypeScript SDK server](https://ts.sdk.modelcontextprotocol.io/server), and [Model Context Protocol](https://modelcontextprotocol.io/docs/).
---
# Deno + OpenAI
Source: /en/docs/ai/openai/
The official OpenAI JavaScript SDK runs through Deno's npm compatibility layer. New text-generation projects should start with the Responses API.
```bash
deno add npm:openai
```
```ts title="main.ts"
import OpenAI from "openai";
const model = Deno.env.get("OPENAI_MODEL");
if (!model) throw new Error("OPENAI_MODEL is required");
const client = new OpenAI(); // reads OPENAI_API_KEY by default
const response = await client.responses.create({
model,
instructions: "Answer accurately and say when evidence is missing.",
input: "Explain Deno permissions in two sentences.",
});
console.log(response.output_text);
```
```bash
OPENAI_MODEL= \
OPENAI_API_KEY= \
deno run --allow-env=OPENAI_MODEL,OPENAI_API_KEY \
--allow-net=api.openai.com:443 main.ts
```
## Why the model is not hard-coded
Model availability, price, and capability change. Keep the model in deployment configuration, select it against your quality, latency, cost, and safety evals, and compare the same eval set before upgrading. A temporary example default should not become a production decision.
## Server boundary
- Keep the API key server-side; never send it to the browser.
- Record request ID, model, latency, token usage, and error class—not secrets or complete sensitive inputs.
- Set a deadline and cancel upstream work when the client disconnects.
- Treat model output as untrusted data; validate before rendering HTML, executing a tool, or writing a database.
- `response.output` can contain several item types; use the SDK's `output_text` aggregate when you only need text.
Official references: [OpenAI text generation](https://developers.openai.com/api/docs/guides/text), [OpenAI JavaScript SDK](https://github.com/openai/openai-node), and [Deno AI examples](https://docs.deno.com/examples/?category=ai).
---
# Deno Sandbox
Source: /en/docs/ai/sandbox/
Deno Sandbox creates on-demand Linux microVMs through `@deno/sandbox` for agent code execution, plugins, and user builds. This is a different layer from Deno runtime permissions: a sandbox supplies a separate VM and resource boundary.
## Create and clean up automatically
```ts
import { Sandbox } from "@deno/sandbox";
await using sandbox = await Sandbox.create({
memoryMb: 2048,
timeout: "10m",
allowNet: ["jsr.io", "registry.npmjs.org"],
labels: { workload: "agent-build" },
});
const result = await sandbox.sh`deno --version`;
console.log(result.stdout);
```
Use `await using` or `finally` to guarantee cleanup. `close()` disconnects; use `kill()` when the VM must terminate early. Do not conflate those lifecycle operations.
## Secure defaults
As of 2026-08-03, official pages disagree about omitted `allowNet`: the creation guide says no outbound network, while the security guide says all outbound requests are allowed. Do not rely on that default; always pass the smallest explicit `allowNet`. Default memory is roughly 1280 MB, while memory, regions, lifetime, and prerelease quotas require a current check.
- Allowlist network hosts and bind each secret to hosts that need it.
- Validate upload paths; cap download size and type.
- Use parameterized APIs or tagged templates, never concatenate user text into a shell.
- Bound wall-clock time, output, concurrency, and disk.
- Put sandbox ID, agent ID, tenant, and request ID in labels and audit logs.
- Promote stable long-running services to a Deploy app instead of using a temporary sandbox as a server.
A model suggesting code execution does not authorize it. Deletion, publishing, network access, customer-data reads, and cost-bearing actions still require business authorization and approval.
Official sources: [Deno Sandbox](https://docs.deno.com/sandbox/), [Create a sandbox](https://docs.deno.com/sandbox/create/), and [Security](https://docs.deno.com/sandbox/security/).
---
# Compile to a single-file executable
Source: /en/docs/core/compile/
`deno compile` packs the entry module graph and the trimmed `denort` runtime into one self-contained executable. Target machines do not need Deno installed.
## Basic usage
```bash
deno compile --output=server main.ts
./server
```
The output is a platform-specific binary. Runtime flags must be declared at compile time, including permissions:
```bash
deno compile --allow-net --allow-env=PORT --output=server main.ts
```
A compiled binary does not accept `--allow-*` flags at runtime. The permission set is fixed when you run `deno compile` and baked into the artifact; it can be neither tightened nor relaxed later. Compile with least privilege and never burn `-A` into a file you distribute.
## Cross-compilation
`--target` cross-compiles from any host platform. Five target triples are currently supported:
| Target | Platform |
| --- | --- |
| `x86_64-pc-windows-msvc` | Windows x86_64 |
| `x86_64-apple-darwin` | macOS x86_64 |
| `aarch64-apple-darwin` | macOS ARM64 |
| `x86_64-unknown-linux-gnu` | Linux x86_64 |
| `aarch64-unknown-linux-gnu` | Linux ARM64 |
```bash
deno compile --target=aarch64-unknown-linux-gnu --output=server-linux-arm64 main.ts
```
The matching `denort` binary is downloaded into the `DENO_DIR` cache. Always boot a cross-compiled artifact once on the real target platform; "it compiled" is not a verification.
## Embedding assets
Since Deno 2.1, `--include` embeds files or directories into the binary, readable through `import.meta`-relative paths:
```bash
deno compile --include=./data --include=worker.ts main.ts
```
```ts
const csv = await Deno.readTextFile(import.meta.dirname + "/data/names.csv");
```
- `--include` can be passed multiple times; only local files are supported, remote modules cannot be embedded.
- Embedded `.js` / `.ts` files become module-graph roots and are transpiled. For pre-built frontend bundles use `--include-as-is` to embed them verbatim.
- The `compile` block in `deno.json` declares `include` / `exclude` arrays, which merge with CLI flags.
- The whole resolved `node_modules` tree is embedded by default; the experimental `--exclude-unused-npm` flag embeds only reachable npm packages.
## Boundary with Docker and Deploy
| Scenario | Preferred approach |
| --- | --- |
| Distributing CLI tools or single-file daemons | `deno compile` |
| Services needing system libraries, CAs, a shell, or multi-process setups | [Docker and containers](/en/docs/deploy/docker) |
| HTTP services needing managed TLS, multi-region rollout, and autoscaling | [Deno Deploy](/en/docs/deploy/deno-deploy) |
`deno compile` solves the distribution shape, not process management, rolling upgrades, or certificates. Long-running services still need systemd, an orchestrator, or a hosted platform.
## Known limitations
- Only string-literal dynamic imports are included statically; computed specifiers must be pulled in with `--include`.
- Worker code is not part of the artifact by default; bring it in via `--include` or a static `import "./worker.ts"`.
- Native plugins (FFI, `.node` addons) rely on self-extracting mode, which slows down first runs and uses extra disk; verify per target platform.
- The binary is bound to a Deno version; runtime upgrades mean recompiling and redistributing.
Official reference: [deno compile](https://docs.deno.com/runtime/reference/cli/compile/).
---
# Debugging and editor setup
Source: /en/docs/core/debugging/
## VS Code extension
Install the official extension `denoland.vscode-deno`, then run **Deno: Initialize Workspace Configuration** from the command palette. It writes this into your workspace `.vscode/settings.json`:
```json
{
"deno.enable": true
}
```
- Enable per workspace only, never in user settings — otherwise every project is treated as a Deno project.
- Once enabled, the extension hands off to the Deno language server and mutes VS Code's built-in TS/JS diagnostics.
- In mixed repositories, use `deno.enablePaths` to activate Deno only in subfolders (e.g. `./supabase/functions`).
Other editors attach to the same language server over LSP:
```bash
deno lsp
```
When something misbehaves, check **Deno: Language Server Status** in the command palette to confirm the active configuration first.
## Inspector breakpoints
Deno speaks the V8 Inspector protocol, with three flags for three startup modes:
| Flag | Behavior |
| --- | --- |
| `--inspect` | Starts the debug server; code runs immediately |
| `--inspect-wait` | Waits for a debugger to attach before running |
| `--inspect-brk` | Waits, then breaks on the first line |
The default address is `127.0.0.1:9229`. Open `chrome://inspect` in a Chromium-based browser and click **Inspect** next to the target to set breakpoints and step through code; source maps show your original TypeScript.
VS Code connects with an attach configuration:
```json title=".vscode/launch.json"
{
"version": "0.2.0",
"configurations": [
{
"name": "Attach to dev server",
"type": "node",
"request": "attach",
"port": 9229
}
]
}
```
```bash
deno run --inspect-wait --allow-net main.ts
```
An inspector exposed on a routable address is remote arbitrary code execution. Inside containers, debug via port forwarding or exec — never ship `--inspect=0.0.0.0:9229` in a production image.
## Tracing permissions and leaks
When a permission error's origin is unclear, ask the runtime for the triggering stack:
```bash
DENO_TRACE_PERMISSIONS=1 deno run main.ts
```
When tests report leaked resources or async ops, trace their source:
```bash
deno test --trace-leaks
```
`--trace-leaks` slows test execution down; remove it once the leak is found instead of leaving it in CI. Both are diagnostics only — their output points at the cause, it does not fix it.
## Logging layers
- During development, `console.log` / `console.error` write to stdout/stderr, which container platforms collect natively.
- In production, emit structured JSON logs (one object per line with `level`, `msg`, `requestId` fields) instead of free text that needs regex parsing.
- Turn on `--log-level=debug` for the Deno runtime's own diagnostics (module resolution, network, permission decisions), and turn it off once the problem is found; third-party library log levels are controlled by each library's own configuration and are out of this flag's reach.
- Logs never contain secrets or personal data; see [Environment variables and .env](/en/docs/core/env-variables).
Official references: [Debugging](https://docs.deno.com/runtime/fundamentals/debugging/), [VS Code](https://docs.deno.com/runtime/reference/vscode/), [deno test](https://docs.deno.com/runtime/reference/cli/test/).
---
# Dependencies, JSR, and npm
Source: /en/docs/core/dependencies/
## Add dependencies
```bash
deno install jsr:@std/assert
deno install npm:express
```
Since Deno 2.8, an unprefixed CLI package name defaults to npm, so `deno install express` equals `deno install npm:express`. Import specifiers still need `npm:` unless mapped by `imports` or `package.json`; JSR packages remain explicit as `jsr:` in the CLI.
The command updates `imports` in `deno.json`, allowing mapped names in code:
```ts
import { assertEquals } from "@std/assert";
import express from "express";
import { readFile } from "node:fs/promises";
```
## Four sources of project state
| Location | Responsibility |
| --- | --- |
| `deno.json(c)` | import mappings, tasks, workspace, and tool settings |
| `package.json` | npm scripts/dependencies and Node ecosystem metadata; may coexist |
| `deno.lock` | resolved versions and integrity; commit it |
| Deno cache / `node_modules` | local materialization, not the review source of truth |
CI should use the committed lockfile and fail on inconsistent resolution. Do not hide resolution conflicts by deleting it.
```bash
deno ci # Deno 2.8+: require lockfile, wipe node_modules, frozen install
deno ci --prod # skip package.json devDependencies
```
## Choose a source
- Preserve the repository's existing source and import conventions.
- Check JSR first for TypeScript-native libraries and the standard library.
- Use npm for Node ecosystem packages; spell built-ins with `node:`.
- URL imports can pin web modules, but team projects usually centralize them through `imports`.
- Deno 2.9 adds `deno list` for declared dependencies and `deno link` / `unlink` for local package links.
When resolution fails, use `deno info` to inspect the graph and cache before checking proxies, private registries, and certificates.
Official references: [Modules and dependencies](https://docs.deno.com/runtime/fundamentals/modules/) and [Packages](https://docs.deno.com/runtime/packages/).
---
# Deno Desktop
Source: /en/docs/core/desktop/
`deno desktop` is available in Deno 2.9+. It packages a TypeScript/JavaScript application, the Deno runtime, and a web rendering backend into a self-contained artifact per platform.
## Shortest workflow
```bash
deno --version # requires 2.9+
deno desktop --hmr main.ts
deno desktop main.ts
```
The entry can be a small `Deno.serve` application or a framework such as Next.js, Astro, Fresh, or SvelteKit. Configure adapters, assets, and development commands from the framework-specific official guide.
## Capabilities and boundaries
| Capability | Current meaning |
| --- | --- |
| Windows | `Deno.BrowserWindow` owns lifecycle and multiple windows |
| Frontend/backend calls | explicit bindings; renderer input is never trusted code |
| Desktop integration | menus, tray, Dock, dialogs, and native notifications |
| Debugging | `--hmr` and unified DevTools |
| Distribution | target triples and `--all-targets` cross-builds |
| Updates | `Deno.autoUpdate()` with bsdiff and failed-launch rollback |
```bash
deno desktop --target aarch64-apple-darwin main.ts
deno desktop --all-targets main.ts
```
## Release checks
- Configure real macOS/Windows code signing; an ad-hoc signature does not remove Gatekeeper warnings.
- Verify notifications, file dialogs, GPU, fonts, and updates on every target OS.
- Sign update manifests with Ed25519 and serve updates over HTTPS.
- Official docs currently say Windows cannot apply a downloaded auto-update patch; verify again before release.
- Error reports may contain stacks and runtime context. Send them only over HTTPS after data classification.
Official sources: [Desktop apps](https://docs.deno.com/runtime/desktop/), [Distribution](https://docs.deno.com/runtime/desktop/distribution/), and [Auto-update](https://docs.deno.com/runtime/desktop/auto_update/).
---
# Environment variables and .env
Source: /en/docs/core/env-variables/
Deno does not load `.env` files by default; you opt in explicitly. Reading environment variables is also gated by the `--allow-env` permission.
## Loading .env files
```bash
deno run --env-file main.ts # discover the first .env from cwd upward
deno run --env-file=.env.local main.ts # explicit file
```
- `--env-file` can be passed multiple times. Within one file the first occurrence of a duplicate wins; across files, the last file specified takes precedence.
- Do not rely on automatic loading: CI and production should consume real environment variables injected by the platform, not files from the repository.
- To load files in code, the standard library `@std/dotenv` works, but it still needs `--allow-read` and `--allow-env`.
## Precise grants
```bash
deno run --allow-env=PORT,API_KEY main.ts
```
```ts title="main.ts"
const port = Number(Deno.env.get("PORT") ?? "8000");
if (!Number.isInteger(port)) throw new Error("PORT must be an integer");
const apiKey = Deno.env.get("API_KEY");
if (!apiKey) throw new Error("API_KEY is required");
```
Validate variables at startup and fail fast; it is far easier to diagnose than an `undefined` surfacing mid-request. List only the variables the program actually reads in `--allow-env`, so third-party dependencies cannot peek at the rest of the environment.
## Common DENO_* runtime variables
| Variable | Purpose |
| --- | --- |
| `DENO_DIR` | Dependency and compile cache directory |
| `DENO_NO_PACKAGE_JSON` | Disable automatic `package.json` resolution |
| `DENO_NO_PROMPT` | Disable permission prompts, equivalent to `--no-prompt` |
| `DENO_NO_UPDATE_CHECK` | Disable new-version checks |
| `DENO_TLS_CA_STORE` | Certificate source, `system` / `mozilla`, defaults to `mozilla` |
| `DENO_CERT` | Load extra CAs from a PEM file, equivalent to `--cert` |
| `DENO_AUTH_TOKENS` | Bearer tokens for private module sources |
| `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` | Proxy settings for module downloads and `fetch` |
| `NO_COLOR` | Disable colored output |
The official documentation is the authoritative full list; do not assume a variable exists from memory in scripts.
## Configuration layering
Following twelve-factor principles, keep every environment-specific value in environment variables:
1. Code may carry only non-sensitive defaults (such as port 8000).
2. Local development uses `.env`, added to `.gitignore`.
3. The repository commits `.env.example` with key names and format notes, never real values.
4. CI and production receive variables injected by the platform and do not read a `.env` from the repo.
Treat any committed secret as compromised: rotate it and scrub git history rather than just deleting the latest commit. Redact at the output layer — never print `Deno.env.toObject()`, and never concatenate tokens into error messages.
## Connecting to Deno Deploy
On Deno Deploy, environment variables are maintained in the console app's **Environment Variables** section, or managed with the `deno deploy env` subcommands (Classic's `deployctl` was retired with the platform — do not use it anymore):
```bash
deno deploy env add API_KEY "sk-..." --secret --org --app
deno deploy env load .env.production --org --app
```
`env add --secret` keeps the value hidden in the dashboard and in list output; `env load` bulk-imports a `.env` file. Code still only calls `Deno.env.get()`. When the local `.env` and the platform use the same key names, the code stays unaware of the deployment shape. See [Deno Deploy](/en/docs/deploy/deno-deploy).
Official reference: [Environment variables](https://docs.deno.com/runtime/reference/env_variables/).
---
# Modules and import maps
Source: /en/docs/core/imports/
Deno-first code uses standard ESM, and local imports include their real extension:
```ts
import { add } from "./math.ts";
import { join } from "jsr:@std/path";
import React from "npm:react";
```
Do not scatter versions through every source file. Use `deno add` to write project aliases to `deno.json`:
```bash
deno add jsr:@std/path npm:react
```
```json
{
"imports": {
"@std/path": "jsr:@std/path@^1.1.2",
"react": "npm:react@^19.2.0",
"@/": "./src/"
}
}
```
Then import stable names:
```ts
import { join } from "@std/path";
import { loadConfig } from "@/config.ts";
```
## Selection order
1. Web standards or built-in Deno APIs;
2. TypeScript-first JSR packages, especially `@std/*`;
3. npm packages through `npm:` or `deno add`;
4. direct HTTPS imports only when necessary, with a trusted pinned source.
A computed dynamic import is outside the static module graph: a local path needs `--allow-read`, while a remote URL needs `--allow-import`. Static imports behave differently.
Official references: [Modules](https://docs.deno.com/runtime/fundamentals/modules/) and [Import maps example](https://docs.deno.com/examples/import_maps_tutorial/).
---
# OpenTelemetry observability
Source: /en/docs/core/observability/
Deno has built-in OpenTelemetry integration for runtime metrics, HTTP traces, `console` logs, and application-defined telemetry over OTLP.
## Shortest verification
```bash
OTEL_DENO=true \
OTEL_EXPORTER_OTLP_PROTOCOL=console \
deno run --allow-net main.ts
```
The console exporter confirms that signals exist. Production commonly targets an OTLP collector:
```bash
OTEL_DENO=true \
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 \
OTEL_SERVICE_NAME=orders-api \
deno run --allow-net=0.0.0.0:8000,otel-collector:4318 main.ts
```
Runtime OTEL configuration and application calls to `Deno.env.get()` are different paths; the latter remains subject to `--allow-env`. In either case, put the telemetry endpoint, headers, and service name in an explicit configuration contract and never log header values.
## Custom spans
```ts
import { trace } from "npm:@opentelemetry/api";
const tracer = trace.getTracer("orders");
await tracer.startActiveSpan("create-order", async (span) => {
try {
span.setAttribute("order.channel", "web");
// business operation
} catch (error) {
span.recordException(error as Error);
throw error;
} finally {
span.end();
}
});
```
## Production boundaries
- Never record tokens, cookies, Authorization, or full prompts as attributes.
- Propagate trace context across requests, databases, and tool calls.
- Bound sampling rate, export timeout, and queues.
- Observe exporter failures without letting telemetry block core requests.
- Use distinct service/environment attributes for local, CI, preview, and production.
Official source: [OpenTelemetry](https://docs.deno.com/runtime/fundamentals/open_telemetry/).
---
# Permissions and security boundaries
Source: /en/docs/core/permissions/
Deno denies sensitive I/O by default. Permissions apply to an execution thread; they are not independent per dependency.
## Common permissions
| Capability | Scoped example |
| --- | --- |
| Read files | `--allow-read=./config,./public` |
| Write files | `--allow-write=./data` |
| Network | `--allow-net=api.example.com:443` |
| Environment | `--allow-env=PORT,API_KEY` |
| Subprocess | `--allow-run=git` |
| Dynamic imports | `--allow-import=jsr.io` |
`--deny-*` takes precedence over its matching `--allow-*`, so broad grants can exclude specific paths or hosts. Use `--no-prompt` in non-interactive environments so missing grants fail immediately.
```bash
deno run \
--allow-net=api.example.com:443 \
--allow-env=API_KEY \
--no-prompt main.ts
```
## High-risk grants
- `-A` / `--allow-all` disables the sandbox.
- A process started with `--allow-run` does not inherit Deno's restrictions; do not let constrained code launch a shell or a new `deno -A`.
- `--allow-ffi` loads native machine code whose system calls are outside the JavaScript permission layer.
- Loading the initial static module graph and runtime I/O are separate boundaries. Successful imports do not grant application network access.
## Verification checklist
1. Run with no grants and identify the denied operation.
2. Add one scoped grant at a time.
3. Use `--no-prompt` in CI.
4. Use OS or container isolation for tools, plugins, and user code; do not rely on Deno permissions alone.
Official references: [Security and permissions](https://docs.deno.com/runtime/fundamentals/security/) and [Permissions reference](https://docs.deno.com/runtime/reference/permissions/).
---
# Runtime and HTTP
Source: /en/docs/core/runtime/
## Run files and tasks
```bash
deno run main.ts
deno run --watch --allow-net main.ts
deno task dev
```
Use `deno run` for a direct entry point and `deno task` to standardize flags and permissions in `deno.json`. Team READMEs and CI should invoke tasks instead of duplicating argument sets.
## Use web-standard APIs
```ts title="main.ts"
const controller = new AbortController();
Deno.serve({ port: 8000, signal: controller.signal }, (request) => {
const url = new URL(request.url);
if (url.pathname === "/health") return Response.json({ ok: true });
return new Response("Not found", { status: 404 });
});
Deno.addSignalListener("SIGTERM", () => controller.abort());
```
```bash
deno run --allow-net main.ts
```
Requests use standard `Request`, `Response`, `URL`, streams, and `fetch`. `Deno.serve` owns the listener; it does not replace authentication, rate limiting, or an operations platform.
## Environment and exit behavior
```ts
const port = Number(Deno.env.get("PORT") ?? "8000");
if (!Number.isInteger(port)) throw new Error("PORT must be an integer");
```
Grant only the required name with `--allow-env=PORT`. Verify exit codes, uncaught exceptions, and signal behavior in the actual deployment environment.
Official references: [Run code](https://docs.deno.com/runtime/run/) and [HTTP server](https://docs.deno.com/runtime/fundamentals/http_server/).
---
# Deno Standard Library
Source: /en/docs/core/standard-library/
The Deno Standard Library is not a set of runtime globals. It is a collection of independently versioned modules under the JSR `@std` scope. Install only the packages the project uses:
```bash
deno add jsr:@std/path jsr:@std/fs jsr:@std/assert
```
```ts
import { join } from "@std/path";
import { ensureDir } from "@std/fs";
import { assertEquals } from "@std/assert";
await ensureDir(join("data", "cache"));
assertEquals(join("data", "file.txt"), "data/file.txt");
```
## Common packages
| Task | Package | Note |
| --- | --- | --- |
| portable paths | `@std/path` | do not concatenate `/` manually |
| file helpers | `@std/fs` | still subject to Deno permissions |
| test assertions | `@std/assert` | pair with `Deno.test` |
| date and time helpers | `@std/datetime` | marked UNSTABLE on JSR and never stabilized with std 1.0; prefer Web `Temporal` / `Intl` |
| UUIDs | Web `crypto.randomUUID()` | usually no package required |
| HTTP | `Deno.serve` | add a framework for routing or sessions |
Standard-library packages have no third-party dependencies and aim to work across Deno, Node, Bun, Workers, and browsers where the underlying platform API exists.
Official references: [Deno Standard Library](https://docs.deno.com/runtime/reference/std/) and [JSR @std](https://jsr.io/@std).
---
# Supply-chain security
Source: /en/docs/core/supply-chain/
Dependency security is a continuous control from resolution and installation through scripts, upgrades, and publishing—not a one-time scan.
## Reproducible installation
Since Deno 2.8, `deno ci` provides an `npm ci`-style strict install: it requires `deno.lock`, removes stale `node_modules`, and installs from a frozen lockfile.
```bash
deno ci
deno ci --prod
deno test
```
Use `deno install` during development and `deno ci` for CI or production builds. Refresh an outdated lockfile explicitly in a development branch and review its diff; never repair it automatically in CI.
## Vulnerabilities and lifecycle scripts
```bash
deno audit
deno audit --socket
deno audit --fix
deno approve-scripts
```
`deno audit --fix` changes manifests and regenerates the lockfile, so treat it as a reviewed upgrade. Deno does not run npm `preinstall` / `postinstall` by default. Approve only a package that genuinely needs scripts:
```bash
deno install --allow-scripts=npm:better-sqlite3
```
## Minimum dependency age
Deno 2.9 skips npm versions published less than 24 hours ago by default. Projects can increase the window:
```json title="deno.json"
{
"minimumDependencyAge": "P3D"
}
```
```ini title=".npmrc"
min-release-age=3
trust-policy=no-downgrade
```
`trust-policy=no-downgrade` prevents a locked package from silently moving from a trusted publication method to a weaker one. It is currently opt-in; first assess provenance coverage across your dependencies.
## Lockfile and vendor
A lockfile pins versions and integrity, but cannot make a build offline when a remote source disappears. `vendor: true` materializes source in the repository. High-assurance environments commit both `deno.lock` and `vendor/`, then enforce frozen resolution in CI.
Official sources: [Supply chain management](https://docs.deno.com/runtime/packages/supply_chain/), [`deno ci`](https://docs.deno.com/runtime/reference/cli/ci/), and [`deno audit`](https://docs.deno.com/runtime/reference/cli/audit/).
---
# Testing, mocks, and coverage
Source: /en/docs/core/testing/
## Minimal test
```ts title="user_test.ts"
import { assertEquals } from "jsr:@std/assert";
Deno.test("normalizes a user name", () => {
assertEquals(" Ada ".trim(), "Ada");
});
```
```bash
deno test
deno test --filter "user"
deno test --watch
```
Deno 2.9 has snapshots on the test context with no extra import:
```ts
Deno.test("renders a card", async (t) => {
await t.assertSnapshot(renderCard({ title: "Deno" }));
});
```
Create or refresh snapshots with `deno test --update-snapshots` and review the `__snapshots__/*.snap` diff. CI runs plain `deno test` and must never update expected output.
Tests are sandboxed too. Grant network or temporary-directory access only to the tests that need it; do not run the whole suite with `-A` because one integration test needs I/O.
## Async cleanup
```ts
Deno.test("fetches health", async () => {
const controller = new AbortController();
try {
// start resource, assert behavior
} finally {
controller.abort();
}
});
```
Deno's sanitizers help detect leaked async operations and resources. Note the defaults: since Deno 2.8, only the exit sanitizer is on by default; the op and resource sanitizers are opt-in. Enable leak detection explicitly on the test or step that needs it instead of relying on defaults:
```ts
Deno.test({
name: "closes every handle",
sanitizeOps: true,
sanitizeResources: true,
fn: async () => {
// ...
},
});
```
Conversely, if a test intentionally keeps background work alive, disable the relevant sanitizer on that test with a note explaining why — never disable them globally.
## Coverage
```bash
deno test --coverage=coverage
deno coverage --lcov --output=coverage.lcov coverage/
```
Coverage does not replace tests for failure paths, denied permissions, timeouts, and cancellation. CI should run at least `fmt --check`, `lint`, `check`, and `test`.
## Large suites
```bash
deno test --changed=origin/main
deno test --shard=1/3
deno test --retry=2
deno test --repeats=3
deno test --trace-leaks
```
`retry` tolerates a known flaky failure and passes after one successful attempt; `repeats` requires every run to pass and helps expose instability. Do not let retries permanently hide a deterministic defect.
Official references: [Testing](https://docs.deno.com/runtime/test/), [Coverage](https://docs.deno.com/runtime/test/coverage/), and [Mocking](https://docs.deno.com/runtime/test/mocking/).
---
# Built-in development tools
Source: /en/docs/core/tooling/
## Common commands
| Goal | Command |
| --- | --- |
| Format / verify format | `deno fmt` / `deno fmt --check` |
| Static rules | `deno lint` |
| Type-check a module graph | `deno check main.ts` |
| Generate API docs | `deno doc --html --name=my-lib mod.ts` |
| Benchmark | `deno bench` |
| Compile an executable | `deno compile --allow-net main.ts` |
| Experimentally bundle a module graph | `deno bundle main.ts -o bundle.js` |
`deno compile` packages the entry graph and runtime, but permissions still follow the official compile/runtime rules. Verify cross-compilation, dynamic assets, and native dependencies separately. The current official docs mark `deno bundle` as experimental; prefer a framework or Vite build for frontend applications.
## Recommended tasks
```json title="deno.json"
{
"tasks": {
"dev": "deno run --watch --allow-net main.ts",
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno test"
}
}
```
Do not run Deno's formatter and another formatter over the same files. During migration, choose one authority before making a mechanical formatting commit.
Official references: [Lint and format](https://docs.deno.com/runtime/lint_and_format/) and [Bundling](https://docs.deno.com/runtime/reference/bundling/).
---
# Built-in TypeScript
Source: /en/docs/core/typescript/
Deno executes `.ts` and `.tsx` files directly, and its binary includes a type checker. You do not need `typescript`, `ts-node`, or `tsx` just to start a project.
```bash
deno run main.ts # execute
deno check main.ts # static type check
deno test --check # test and check the test module graph
```
Run `deno check` explicitly in the current workflow. Do not treat a successful `deno run` as full type validation or wait for production startup to reveal errors.
## Configuration rule
Deno-first projects usually keep a small set of TypeScript options in `deno.json`:
```json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true
}
}
```
An existing Node project's `tsconfig.json` can still be read. When `deno.json` also contains `compilerOptions`, it takes precedence. Emit settings do not apply because Deno does not use the checker to write JavaScript output.
## Web, worker, and DOM types
The default type environment targets the Deno runtime and does not include browser `document`. Configure `lib` explicitly for shared browser code instead of hiding environment differences with global declarations.
Official references: [TypeScript support](https://docs.deno.com/runtime/fundamentals/typescript/) and [Configuring TypeScript](https://docs.deno.com/runtime/reference/ts_config_migration/).
---
# Workspaces and monorepos
Source: /en/docs/core/workspaces/
## Minimal structure
```json title="deno.json"
{
"workspace": ["./apps/api", "./packages/core"],
"tasks": {
"check": "deno task --recursive check"
}
}
```
Members may have their own `deno.json` and tasks. A workspace shares the lockfile and root configuration while preserving package boundaries.
```text
repo/
├── deno.json
├── deno.lock
├── apps/api/deno.json
└── packages/core/deno.json
```
## Design principles
- Root tasks orchestrate; member tasks verify each package.
- Resolve internal packages through the workspace instead of publishing intermediary versions.
- Public packages declare `exports`, licensing, and publish contents.
- Do not put broad permissions in the root when most members do not need them.
## Verify
```bash
deno task --recursive check
deno test
```
Running `deno test` at the workspace root discovers member tests; `deno task --recursive` invokes matching member tasks. Filtering and npm workspace interop evolve, so run `deno help task` and verify the current guide for complex graphs.
Official reference: [Workspaces](https://docs.deno.com/runtime/fundamentals/workspaces/).
---
# Choose a database for Deno
Source: /en/docs/database/
Deno can use mature database drivers through npm compatibility as well as Web and JSR modules. Choose from the data model, transaction needs, and hosting environment—not from which package looks most “Deno-native.”
| Data layer | Good for | Deno access |
| --- | --- | --- |
| PostgreSQL | relational data, transactions, complex queries | `npm:postgres`, `npm:pg`, Drizzle, Kysely |
| [SQLite](/en/docs/database/sqlite) | single-node, embedded, tests, small services | `node:sqlite` or compatible libraries; verify runtime version |
| MongoDB | document models and existing Mongo stacks | official npm driver or Mongoose |
| Redis | cache, rate limits, short-lived state | official npm client or compatible service |
| [Supabase](/en/docs/database/supabase) | managed Postgres plus Auth/Storage | official npm SDK or direct Postgres |
| [Deno KV](/en/docs/database/kv) | simple key-value and atomic operations | `Deno.openKv()`; verify hosting support first |
## Common connection boundary
```bash
deno run \
--allow-env=DATABASE_URL \
--allow-net=db.example.com:5432 \
src/main.ts
```
- Reuse a pool over the module or application lifecycle; do not create one per request.
- Load secrets only from environment or a secret store, never source, logs, or error responses.
- Treat migrations as a release step; schema mutation during app startup multiplies concurrency risk.
- Serverless instances scale concurrently, so budget connections across the maximum instance count.
- Test connection failure, rollback, unique constraints, and timeouts.
The new Deno Deploy supports linked PostgreSQL, timeline isolation, and Deno KV, but data and migration policies still need to be designed per timeline. See [Deploy data and cron](/en/docs/deploy/data-and-cron).
Official references: [Connecting to databases](https://docs.deno.com/examples/connecting_to_databases_tutorial/) and [Deno database examples](https://docs.deno.com/examples/?category=databases).
---
# Deno KV
Source: /en/docs/database/kv/
Deno KV is a key-value store built into the runtime. Locally you open it with `Deno.openKv()`; on the new Deno Deploy it is available as a linkable database engine. As of 2026-08-03, Deno still marks KV as in development and subject to change, and local runs require `--unstable-kv`.
```ts
const kv = await Deno.openKv();
await kv.set(["sessions", sid], { userId, createdAt: new Date() });
const entry = await kv.get(["sessions", sid]);
const result = await kv.atomic()
.check(entry) // commits only if the versionstamp is unchanged
.set(["sessions", sid], { userId, rotatedAt: new Date() })
.commit();
```
Local development: `Deno.openKv("./data/app.kv")` persists to a file; `Deno.openKv(":memory:")` is for tests.
```bash
# KV's underlying disk access needs no --allow-read/--allow-write (the official
# permissions docs say so explicitly); grant only what your app's own I/O requires
deno run --unstable-kv main.ts
```
Deploy Classic shut down on 2026-07-20. The new Deno Deploy supports Deno KV as a database engine linked to an app, and `Deno.openKv()` automatically connects to the isolated logical database for the current timeline. But the official migration guide states: Classic KV data is not migrated automatically—contact support@deno.com for assistance; KV Queues (`kv.enqueue()` / `kv.listenQueue()`) are not supported on the new platform; and an app can currently link only one database instance, so it cannot have both KV and PostgreSQL. Redesign before migrating if you depend on any of these.
## API essentials
- Keys are arrays: `["users", userId]`. Parts may be string, number, boolean, bigint, or Uint8Array, ordered lexicographically with type order taking precedence over value order.
- Values must be structured-clone compatible (objects, arrays, Map, Set, Date, Uint8Array, etc.); class instances, functions, and Symbols are not supported.
- `get` / `getMany` / `list` for reads. `list` scans by prefix or range in batches (default 500) and does not guarantee a single snapshot across batches.
- `set` / `delete`, plus `sum` / `min` / `max` (only inside atomic operations, only on `Deno.KvU64`).
- `atomic()` provides optimistic concurrency control: `.check()` asserts versionstamps, and on a failed `.commit()` you re-read and retry. There are no lock-based interactive transactions.
- `watch(keys)` returns a `ReadableStream` that pushes changes; rapid successive writes may be coalesced, so you are not guaranteed every intermediate state.
## Consistency model
- Writes are always strongly consistent.
- Reads can be strong (guaranteed to return the most recently written value) or eventual (faster, may return a stale value); `get` is a snapshot read in all consistency modes.
- Every write receives a monotonically increasing, non-sequential 12-byte versionstamp; all writes in one transaction share it.
Key limits from the official transactions documentation: max key size 2 KiB, max value size 64 KiB; `getMany` reads at most 10 keys; `list` batches at most 1000; a single atomic operation allows at most 100 checks, 1000 mutations, and 800 KiB total; `watch` accepts at most 10 keys.
## KV on the new Deno Deploy
- KV is provisioned through the organization's databases feature and assigned to an app; production, branch, and preview timelines automatically get isolated logical databases.
- Connecting from outside Deploy (for example, from a local CLI against the hosted database): use the connect URL `https://api.deno.com/v2/databases//connect` with a personal or organization access token in the `DENO_KV_ACCESS_TOKEN` environment variable.
- Data residency: per the official docs, KV's primary region is Northern Virginia in the US, with read replicas in Europe and Asia. Writes are stored in and transit through the US, so workloads requiring strict EU residency are not suitable—the docs recommend their Postgres offerings instead.
- Current limitations: one database instance per app; the preview database is shared across all preview deployments; the database explorer does not support KV yet.
## When KV is the wrong choice
- You need queues: `enqueue` / `listenQueue` are unavailable on the new platform—use an external queue or a database-backed job table.
- You need relational queries, joins, or ad-hoc SQL analysis: KV offers key lookups and prefix scans only, and secondary indexes are your own maintenance burden.
- Individual values exceed 64 KiB, or a transaction would exceed the documented atomic limits.
- Strict EU data-residency compliance.
- One app needs both KV and PostgreSQL (current platform limitation).
- You need a frozen API: Deno still flags KV as subject to change.
Official references: [Deno KV overview](https://docs.deno.com/deploy/kv/), [KV operations and consistency](https://docs.deno.com/deploy/kv/operations/), [KV transactions and limits](https://docs.deno.com/deploy/kv/transactions/), [Key space and value types](https://docs.deno.com/deploy/kv/key_space/), [Deno KV on the new Deploy](https://docs.deno.com/deploy/reference/deno_kv/), [Classic migration guide](https://docs.deno.com/deploy/migration_guide/).
---
# PostgreSQL and Drizzle CRUD
Source: /en/docs/database/postgres-drizzle/
Use `postgres` directly for simple queries. Add Drizzle when the project needs a typed schema and migrations.
```bash
deno install npm:drizzle-orm npm:drizzle-kit npm:postgres
```
## Schema
```ts title="src/db/schema.ts"
import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core";
export const posts = pgTable("posts", {
id: integer().primaryKey().generatedAlwaysAsIdentity(),
title: text().notNull(),
body: text().notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});
```
## Connection and query
```ts title="src/db/client.ts"
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema.ts";
const url = Deno.env.get("DATABASE_URL");
if (!url) throw new Error("DATABASE_URL is required");
const client = postgres(url, { max: 5 });
export const db = drizzle(client, { schema });
export const closeDb = () => client.end();
```
```ts
import { eq } from "drizzle-orm";
import { db } from "./client.ts";
import { posts } from "./schema.ts";
const [created] = await db.insert(posts)
.values({ title: "Hello Deno", body: "First post" })
.returning();
const result = await db.select().from(posts).where(eq(posts.id, created.id));
```
## Migrations and permissions
Node-oriented tools such as `drizzle-kit` may need local `node_modules`. Put generation and migration in a separate task or CI job, review SQL before applying it, and do not grant schema mutation to the production HTTP process.
```bash
deno run --allow-env=DATABASE_URL --allow-net=db.example.com:5432 src/script.ts
```
Official references: [Deno Drizzle tutorial](https://docs.deno.com/examples/drizzle_tutorial/), [Postgres example](https://docs.deno.com/examples/postgres/), and [Drizzle PostgreSQL](https://orm.drizzle.team/docs/get-started-postgresql).
---
# SQLite with node:sqlite
Source: /en/docs/database/sqlite/
Deno has shipped `node:sqlite` in its Node compatibility layer since v2.2, and the official Node API docs list it as fully supported. Upstream, Node added the module in v22.5.0 and—as of 2026-08-03—still labels it Stability 1.2 (release candidate), so minor API changes remain possible before full stabilization. Recheck after Deno upgrades.
```ts title="src/db.ts"
import { DatabaseSync } from "node:sqlite";
const db = new DatabaseSync("./data/app.db");
db.exec(`
PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL
) STRICT;
`);
const insert = db.prepare("INSERT INTO posts (title) VALUES (?)");
insert.run("Hello Deno");
const rows = db.prepare("SELECT id, title FROM posts").all();
```
Every `DatabaseSync` API runs synchronously, which suits scripts, CLIs, and single-writer services. Do not put long synchronous queries on a high-concurrency request path.
## Alternatives
| Library | Form | Notes |
| --- | --- | --- |
| `node:sqlite` | built-in Node compat module | no dependency to install; upstream still a release candidate |
| `jsr:@db/sqlite` | FFI loading a prebuilt native library | its README requires `--allow-ffi` and `--allow-env`, plus network and file permissions to download and cache the native library |
| `npm:better-sqlite3` | Node native addon | relies on Deno's native addon support; verify against your Deno version |
FFI and native addons load machine code that JavaScript-level permissions cannot sandbox; count that in your trust boundary. Pure WASM options such as `npm:sql.js` avoid FFI, but the database lives in memory and persistence is your own problem.
## When SQLite fits
- Single-node services, CLI tools, desktop or single-instance edge apps.
- Tests and local development: isolate each test with `:memory:` or a temp file.
- Read-heavy embedded workloads, with WAL to overlap reads and writes.
- Multi-region or edge replicas are the domain of LiteFS/libsql-style solutions, outside `node:sqlite`'s scope—evaluate them separately.
## Least privilege
```bash
deno run --allow-read=./data --allow-write=./data src/main.ts
```
- WAL mode creates `app.db-wal` and `app.db-shm` sidecar files. Granting write access to exactly `app.db` fails at checkpoint or first write; grant the directory, or list all three files.
- Read-only tools should open with `new DatabaseSync(path, { readOnly: true })` and request only `--allow-read`.
- FFI-based options like `jsr:@db/sqlite` effectively need near `-A` trust; do not use narrow grants to reassure yourself.
## WAL, backup, and concurrent-write boundaries
- SQLite has a single writer: WAL lets reads overlap with one write, but writes still serialize. Keep write transactions short; `busy_timeout` controls how long lock contention waits before raising `SQLITE_BUSY`.
- Multiple processes may open the same file, but write throughput does not scale. Never run SQLite on network filesystems such as NFS.
- Run `PRAGMA wal_checkpoint(TRUNCATE)` before a file-copy backup, or the copy misses commits still in the WAL; alternatively use the official SQLite CLI `.backup`.
- Upstream `node:sqlite` added `sqlite.backup()` in Node v23.8.0 / v22.16.0; verify support on your Deno version before relying on it.
- Treat migrations as a release step, as with PostgreSQL—schema changes at app startup multiply concurrency risk across instances.
Official references: [Deno Node API support](https://docs.deno.com/runtime/reference/node_apis/), [Node.js node:sqlite docs](https://nodejs.org/api/sqlite.html), [jsr:@db/sqlite](https://jsr.io/@db/sqlite), [SQLite WAL mode](https://sqlite.org/wal.html).
---
# Supabase
Source: /en/docs/database/supabase/
There are two ways in: treat Supabase as managed Postgres and connect directly (with Drizzle/postgres.js—see [PostgreSQL and Drizzle](/en/docs/database/postgres-drizzle)), or use the official SDK `@supabase/supabase-js` against the PostgREST/Auth/Storage APIs. The SDK is published on both npm and JSR (`jsr:@supabase/supabase-js`).
```ts title="src/supabase.ts"
import { createClient } from "jsr:@supabase/supabase-js@2";
const url = Deno.env.get("SUPABASE_URL");
const key = Deno.env.get("SUPABASE_PUBLISHABLE_KEY");
if (!url || !key) throw new Error("SUPABASE_URL / SUPABASE_PUBLISHABLE_KEY required");
export const supabase = createClient(url, key);
```
```bash
deno run \
--allow-env=SUPABASE_URL,SUPABASE_PUBLISHABLE_KEY \
--allow-net=.supabase.co \
src/main.ts
```
## Direct Postgres connections and pooling
Supabase offers several connection methods; ports and use cases per the official docs:
| Method | Host:port | Use for |
| --- | --- | --- |
| Direct connection | `db..supabase.co:5432` | persistent servers, migrations, `pg_dump`; IPv6 (IPv4 requires a paid add-on) |
| Supavisor session mode | `aws-.pooler.supabase.com:5432` | fallback for IPv4-only networks |
| Supavisor transaction mode | `aws-.pooler.supabase.com:6543` | serverless / edge functions with many short-lived connections |
- For environments without persistent processes (such as Deno Deploy), use transaction mode (6543). The docs state transaction mode does not support prepared statements—disable them in your client (`postgres.js`: `postgres(url, { prepare: false })`).
- Under serverless concurrency, budget total connections against the maximum instance count, not a single instance.
- Run migrations over a direct or session-mode connection as a separate release step; never grant schema mutation to the production HTTP process.
```bash
deno run \
--allow-env=DATABASE_URL \
--allow-net=aws-.pooler.supabase.com:6543 \
src/main.ts
```
## Auth and RLS boundaries
- The SDK acts as the caller: with the new publishable key (the counterpart of the legacy anon JWT key — the two are distinct key types that can coexist, not a simple rename) plus a user JWT, PostgREST operates under that user's role and RLS policies apply.
- The official RLS docs require RLS on every table in an exposed schema (`public` by default); once enabled, the API serves nothing to publishable-key requests until policies exist. Tables created in the Table Editor get RLS automatically; tables created via raw SQL need an explicit `enable row level security`.
- The new secret key (the counterpart of the legacy service_role JWT key) bypasses RLS, and the docs forbid shipping it to browsers or customers. Keep it server-side only, injected from the environment, out of logs.
- If your Deno backend only makes trusted service-to-service calls, a direct Postgres connection is usually simpler than service key + PostgREST. Reach for the SDK + RLS when you need per-end-user authorization.
## Edge Functions vs. local Deno
Supabase Edge Functions run on the Supabase Edge Runtime, which the docs describe as a Deno-compatible, TypeScript-first runtime (open source at github.com/supabase/edge-runtime). Practical differences from local Deno:
- Imports support `npm:`, `jsr:`, and Node built-ins; the docs recommend a dedicated `deno.json` per function and warn against sharing one global config across `/supabase/functions` for deployment.
- Use `supabase functions serve` locally for a runtime close to production, and `supabase functions deploy` to ship.
- Functions are designed for short-lived, idempotent work and can cold-start; move long-running jobs to background workers instead.
- It is a Deno-compatible runtime, not Deno itself—do not assume feature parity with your local `deno` version; check the Edge Runtime repo before relying on newer APIs.
## Secret management
- Do not introduce the secret key where publishable-key access suffices; keep the two under distinct environment variable names to prevent mix-ups.
- Load secrets only from the environment or a secret store—never source, logs, or error responses—and rotate using Supabase's key-rolling flow in the dashboard.
- Scope `--allow-env` to exact variable names and `--allow-net` to the project host; add `--no-prompt` in CI.
Official references: [Connecting to your database](https://supabase.com/docs/guides/database/connecting-to-postgres), [Row Level Security](https://supabase.com/docs/guides/database/postgres/row-level-security), [Edge Functions](https://supabase.com/docs/guides/functions), [Function dependencies](https://supabase.com/docs/guides/functions/dependencies), [Deno's official Supabase example](https://docs.deno.com/examples/supabase/).
---
# Deployment decisions
Source: /en/docs/deploy/
## Choose the runtime model first
| Goal | Consider first | Verify |
| --- | --- | --- |
| Git-driven multi-region (US/EU) runtime and managed data | Deno Deploy | current regions, quotas, build and runtime APIs |
| System packages, Kubernetes, portable image | Docker | image variant, signals, filesystem user, health checks |
| Existing VM or PaaS | native Deno or container | installed version, persistent volumes, network policy |
| Single-file CLI | `deno compile` | target architecture, dynamic assets, permissions, native code |
## Platform-independent contract
Document at least:
- entry point and start command;
- bind address and `PORT` behavior;
- required Deno permissions;
- environment variable names, with no secrets in images or logs;
- `/health` or an equivalent platform signal;
- SIGTERM, timeout, retry, and idempotency boundaries;
- lockfile, Deno version, and rollback artifact.
A platform may use Deno to install or build and execute requests in a different isolate. Verify the build command, request runtime, Web/Node API support, and persistence semantics separately.
Continue with [Deno Deploy](/en/docs/deploy/deno-deploy), [Data and cron](/en/docs/deploy/data-and-cron), [Classic migration](/en/docs/deploy/classic-migration), [Docker](/en/docs/deploy/docker), or the [production baseline](/en/docs/deploy/production-baseline).
---
# CI/CD in practice
Source: /en/docs/deploy/ci-cd/
Prerequisites: `deno.json` and `deno.lock` are committed and local gates pass. This page covers pipeline orchestration only; the gates themselves and the security baseline are in [Production engineering baseline](/en/docs/deploy/production-baseline).
## Install and pin Deno
Use the official action on GitHub Actions and pin the major version:
```yaml
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
```
- `deno-version` accepts `v2.x`, `v2.1.x`, an exact version, or `lts`. For strict reproducibility pin an exact version and review upgrades as standalone changes.
- `deno-version-file` reads the version from files like `.tool-versions` to keep CI and local machines aligned.
## Cache dependencies
`setup-deno` has built-in caching — no hand-written `actions/cache` needed:
```yaml
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
cache: true
```
`cache: true` caches Deno's downloaded dependencies (the `DENO_DIR` contents), keyed by job id, runner OS/arch, and a hash of `deno.lock`. Use `cache-hash` to override the hash (setting it implies caching). If the workflow sets the `DENO_DIR` environment variable itself, make sure the action and later steps use the same directory.
## Install dependencies: deno ci
```bash
deno ci
```
`deno ci` (Deno 2.8+) is the reproducible install command for CI and Dockerfiles: it errors when `deno.lock` is missing, removes any existing `node_modules`, and installs with frozen semantics — the lockfile must match the config file exactly, and any drift fails instead of silently updating. Add `--prod` when building production artifacts to skip devDependencies; excluding `@types/*` as well requires a separate `--skip-types` — it decides by package-name heuristics and may wrongly skip packages that ship runtime code, so verify the output is still complete before relying on it.
## Gate order
Order gates cheapest-and-fastest first:
```bash
deno ci
deno fmt --check
deno lint
deno check "**/*.ts" "**/*.tsx"
deno test
```
Format and lint finish in seconds and catch mechanical issues; type checking catches interface errors; tests are the most expensive and run last. Do not merge the gates into one command — keeping them separate makes the failing layer obvious in CI logs. Quote the globs so Deno expands them (a bare `**/*.ts` is expanded by the shell, which behaves inconsistently across runners and misses `.tsx`); projects that ship their own `check` task, such as Fresh, can simply run `deno task check`.
## Cross-OS/arch matrix
Libraries and CLIs should run on at least three systems:
```yaml
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
```
Watch out for CRLF on Windows: set `git config --system core.autocrlf false` before checkout so `deno fmt --check` does not fail on line endings. Add a canary Deno version with `continue-on-error` to spot upstream changes early without blocking merges. Restrict once-only steps like coverage reports with `if: matrix.os == 'ubuntu-latest'`.
## Build and release artifacts
- Static sites: `deno task build` produces the output directory; pass it along with `actions/upload-artifact` or hand it directly to the deploy step.
- Single-file binaries: `deno compile --target ` cross-compiles per-platform artifacts to attach to a release. Check current flags with `deno help compile`.
- Publishing JSR packages: do not publish on every push. Trigger on tags and publish with OIDC to get provenance — full setup in [Publishing JSR packages](/en/docs/reference/publishing-jsr).
## Deploy to Deno Deploy
Two paths; pick one per team preference:
1. **Built-in GitHub integration (the default path)**: link the app to a GitHub repository in the Deno Deploy console; every push triggers a build, with no deploy YAML to maintain.
2. **Deploying from external CI**: when you need a custom pipeline (for example, running the full matrix before release), use the `deno deploy` CLI:
```bash
deno deploy --org --app --prod
```
CLI authentication in CI uses an organization token: create one, store it as a GitHub repository secret, and pass it through the `DENO_DEPLOY_TOKEN` environment variable. Note that the OIDC page in the Deno Deploy docs covers running apps authenticating to third-party services (AWS, Vault, etc.) — it is not the CLI deploy authentication mechanism; do not conflate the two. Deploy Classic was officially shut down on 2026-07-20; do not use deployctl in new projects.
## Complete example
```yaml title=".github/workflows/ci.yml"
name: ci
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
# Turn off CRLF conversion on Windows runners first, or fmt --check will false-positive
steps:
- run: |
git config --system core.autocrlf false
git config --system core.eol lf
- uses: actions/checkout@v7
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
cache: true
- run: deno ci
- run: deno fmt --check
- run: deno lint
- run: deno check "**/*.ts" "**/*.tsx"
- run: deno test
deploy:
needs: test
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v7
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- run: deno ci --prod
- run: deno task build # projects with a build step
- run: deno deploy --org my-org --app my-app --prod
env:
DENO_DEPLOY_TOKEN: ${{ secrets.DENO_DEPLOY_TOKEN }}
```
Key points: `environment: production` enables GitHub environment protection rules for manual approval; the token is scoped to the target organization; the deploy job uses the same Deno major version as the test matrix.
`deno deploy --prod` and `deno publish` affect external users immediately. Agents and automation scripts must not bypass environment approvals to trigger production deploys.
Official references: [Continuous integration](https://docs.deno.com/runtime/reference/continuous_integration/), [setup-deno](https://github.com/denoland/setup-deno), [Deno 2.8 release notes (deno ci)](https://deno.com/blog/v2.8), [deno deploy CLI reference](https://docs.deno.com/runtime/reference/cli/deploy/), [Deno Deploy changelog](https://docs.deno.com/deploy/changelog/).
---
# Migrate from Deploy Classic
Source: /en/docs/deploy/classic-migration/
Deno's official documentation lists **2026-07-20** as the shutdown date for Deploy Classic and Subhosting v1. That date has passed. If any Classic resource remains, do not assume it was migrated automatically—confirm its status in `console.deno.com` and current official status, then follow this inventory.
## It is not an in-place upgrade
- Classic projects do not automatically become new apps; the new platform requires an organization.
- Replace `deployctl` with built-in `deno deploy`.
- GitHub integration uses integrated builds; do not preserve assumptions from the old Action workflow.
- Split the single Classic environment set into Production, Development, and Build contexts.
- Reconfigure custom domains while preserving a DNS propagation window.
## Code and data differences
```diff
- import { serve } from "https://deno.land/std/http/server.ts";
- serve(() => new Response("hello"));
+ Deno.serve(() => new Response("hello"));
```
- Legacy std `serve()` can time out during new Deploy warmup. Upgrade dependencies and use `Deno.serve()`.
- `Deno.cron()` remains available, but recheck UTC, retries, and timeline behavior.
- Classic KV data is not migrated automatically; coordinate with official support and verify integrity.
- New Deploy currently does not support `Deno.Kv.enqueue()` / `listenQueue()`; move to an external broker or database job queue.
- Subhosting v1 project/deployment becomes v2 app/revision and requires explicit API/SDK mapping.
## Cutover checklist
1. Inventory projects, domains, variables, cron, KV, queues, regions, and Subhosting calls.
2. Create apps in the new organization and verify build/runtime on a non-production timeline.
3. Migrate secrets and contexts without logging their values.
4. Migrate data and background work, then reconcile counts, hashes, or business invariants.
5. Configure certificate challenges and DNS with a rollback window.
6. Verify logs, traces, metrics, alerts, and cost before retiring old resources.
Official sources: [Migration guide](https://docs.deno.com/deploy/migration_guide/) and [About Deno Deploy](https://docs.deno.com/deploy/).
---
# Databases, cron, and timelines
Source: /en/docs/deploy/data-and-cron/
The new Deno Deploy is organized around apps, revisions, timelines, and contexts. Production, Git branches, and previews can serve different revisions with separate environment values and logical databases.
## Databases
The platform currently supports associated PostgreSQL or Deno KV databases. Production, branch, and preview timelines receive isolated logical databases for each app.
```ts
const kv = await Deno.openKv();
Deno.serve(async () => {
await kv.set(["health", "lastSeen"], new Date().toISOString());
return Response.json({ ok: true });
});
```
PostgreSQL connections inject standard `DATABASE_URL`, `PGHOST`, `PGPORT`, `PGDATABASE`, `PGUSER`, and `PGPASSWORD`. A migration task can run as a pre-deploy command before a revision serves traffic.
As of 2026-08-03, official docs say one app cannot associate multiple database instances, so it cannot attach Deno KV and PostgreSQL instances at the same time. All preview deployments for an app also currently share one preview database. Verify again before release.
## Cron
```ts
Deno.cron("daily cleanup", "0 3 * * *", async () => {
await runCleanup();
});
```
Cron schedules use UTC. Deploy discovers jobs during deployment, handles scheduling, and exposes runs in dashboard logs and traces. Failures are not retried by default; opt in per job with `backoffSchedule` (up to 5 attempts, each delay capped at 1 hour). Jobs must be idempotent; when a retry overlaps the next scheduled run, the later invocation may be skipped.
## Context and environment
- Production: the production timeline.
- Development: branch and preview timelines.
- Build: visible only during builds, not automatically at runtime.
- Secrets are hidden in the UI after creation; never echo them through logs.
Use `DENO_TIMELINE`, `DENO_DEPLOY_APP_ID`, and deployment identifiers as observability dimensions, not access-control decisions.
Official sources: [Databases](https://docs.deno.com/deploy/reference/databases/), [Cron](https://docs.deno.com/deploy/reference/cron/), [Timelines](https://docs.deno.com/deploy/reference/timelines/), and [Environment contexts](https://docs.deno.com/deploy/reference/env_vars_and_contexts/).
---
# Deno Deploy
Source: /en/docs/deploy/deno-deploy/
Deno Deploy is Deno's managed platform and can also self-host regions on your infrastructure. The new console is at `console.deno.com`, with an organization → app → revision → timeline model. It is not an in-place upgrade of Deploy Classic.
## Create and deploy
Connect GitHub for integrated builds or use the built-in Deno CLI:
```bash
deno deploy
deno deploy --prod
deno deploy logs --org my-org --app my-app
```
The interactive CLI stores authentication in the system keyring. Automation should use the platform's approved token flow; never put a deploy token in shell history.
| New-platform capability | Official state as of 2026-08-03 |
| --- | --- |
| Environments | separate Production, Development, and Build contexts |
| Data | PostgreSQL or Deno KV with timeline-level logical isolation |
| Scheduling | `Deno.cron()` with runs in logs and traces |
| Observability | dashboard logs, traces, and metrics |
| Caching | CDN caching and Web Cache API |
| Regions | managed US/EU plus self-hosted regions; verify current list |
| Queue | new Deploy does not currently support the old Deno KV Queue API |
## Before release
1. Expose HTTP through `Deno.serve` or the chosen framework.
2. Define reproducible install/build/start behavior in `deno.json`.
3. Commit `deno.lock` and pass CI.
4. Store only secret names in the repository, never secret values.
5. Verify consistency and quotas for KV, databases, and cron; design a replacement before moving a Classic queue workload.
## Release checks
```text
GET /health → 200 with a small stable body
request without key → explicit failure without configuration leakage
timeout/cancellation → abort upstream work
repeated write → idempotent or detectable
rollback → previous artifact can be reactivated
```
Inspect build and request logs for tokens, authorization headers, and user input. Add timeouts to external APIs and idempotency keys to retryable writes.
## Revisions and rollback
Each timeline has revision history and an active revision serving traffic. Verify on a non-production timeline before promotion. Activating an earlier revision does not automatically roll back a database migration.
## Deno KV
`Deno.openKv()` uses different storage backends locally and on the managed platform. Keys are structured arrays and transactions use versionstamps. Prefer an in-memory or isolated database in tests; verify current Deploy quotas and consistency guidance before production.
Continue with [Databases, cron, and timelines](/en/docs/deploy/data-and-cron) and [Classic migration](/en/docs/deploy/classic-migration).
Official sources: [About Deno Deploy](https://docs.deno.com/deploy/), [`deno deploy` CLI](https://docs.deno.com/runtime/reference/cli/deploy/), and [Timelines](https://docs.deno.com/deploy/reference/timelines/).
---
# Docker and containers
Source: /en/docs/deploy/docker/
## Multi-stage example
```dockerfile
FROM denoland/deno:alpine AS build
WORKDIR /app
# Copy the manifest and lockfile and install dependencies first, so source changes don't invalidate the dependency layer
COPY deno.json deno.lock ./
RUN deno ci
COPY . .
RUN deno install --entrypoint main.ts
FROM denoland/deno:distroless
WORKDIR /app
COPY --from=build /app /app
# The dependency cache lives in DENO_DIR (/deno-dir), not /app — copy it explicitly
COPY --from=build /deno-dir /deno-dir
USER deno
EXPOSE 8000
CMD ["run", "--cached-only", "--allow-net", "--allow-env=PORT", "main.ts"]
```
`deno install` caches into the global `DENO_DIR` (`/deno-dir` in the official images), not the project directory. Skipping this copy while keeping `--cached-only` makes the container fail at startup with a missing cache instead of downloading dependencies.
Choose `debian`, `alpine`, or `distroless` based on dependencies. If you need a shell, CAs, fonts, or system libraries, do not choose a less observable variant solely for image size.
## Production checks
- Pin a Deno image tag or digest; do not release from floating `latest`.
- Exclude `.git`, secrets, coverage, and local caches with `.dockerignore`.
- Run as non-root with a read-only root filesystem; mount only required write paths.
- Make `0.0.0.0` and `PORT` behavior explicit in the application.
- Let the platform send SIGTERM and measure graceful shutdown.
- If runtime dependency resolution has no network, cache during build and verify with `--cached-only`.
Official reference: [Deno and Docker](https://docs.deno.com/runtime/reference/docker/).
---
# Production engineering baseline
Source: /en/docs/deploy/production-baseline/
## CI gate
```bash
deno --version
deno ci
deno audit
deno fmt --check
deno lint
deno check **/*.ts
deno test
```
Pin Deno in CI, commit `deno.lock`, and never update dependencies as an automatic response to a failed build. Review dependency upgrades as separate changes.
## Runtime baseline
- Encode least privilege in a task or container `CMD`; add `--no-prompt` in production.
- Validate input, environment, and external responses at their boundaries.
- Give every external request a timeout, cancellation path, and bounded retry policy.
- Log request ID, status, duration, and error class, but not secrets or sensitive bodies.
- Separate liveness from readiness.
- Set memory, CPU, concurrency, and request-body limits around the process.
## Supply chain
Review maintenance, publisher, license, and lifecycle scripts for JSR/npm dependencies. Native addons and FFI bypass the JavaScript permission layer and require extra audit and OS isolation.
Official references: [Continuous integration](https://docs.deno.com/runtime/reference/continuous_integration/) and [Security](https://docs.deno.com/runtime/fundamentals/security/).
---
# Your first Deno project
Source: /en/docs/getting-started/first-project/
Initialize a project:
```bash
deno init hello-deno
cd hello-deno
```
```text
hello-deno/
├── deno.json
├── main.ts
└── main_test.ts
```
Make `main.ts` expose a testable handler:
```ts
export function handler(request: Request): Response {
const url = new URL(request.url);
return Response.json({ message: "Hello Deno", path: url.pathname });
}
if (import.meta.main) {
Deno.serve({ port: 8000 }, handler);
}
```
Record team commands in `deno.json`:
```json
{
"tasks": {
"dev": "deno run --watch --allow-net=0.0.0.0:8000 main.ts",
"check": "deno fmt --check && deno lint && deno check main.ts main_test.ts",
"test": "deno test"
}
}
```
```bash
deno task check
deno task test
deno task dev
```
Listening with `Deno.serve` needs network permission. Unit tests that call `handler()` directly do not open a port, so they do not need `-A`.
Next: [permissions](/en/docs/core/permissions), [web stack selection](/en/docs/web), and [project blueprints](/en/docs/projects).
---
# Install Deno
Source: /en/docs/getting-started/installation/
## Choose an installation method
```bash
brew install deno
```
```bash
curl -fsSL https://deno.land/install.sh | sh
```
```powershell
irm https://deno.land/install.ps1 | iex
```
For teams and CI, pin the runtime with a version manager, container image, or CI action instead of relying on whatever latest version happens to be installed.
## Verify the environment
```bash
deno --version
deno info
deno help
```
Upgrade an installation made with the official installer:
```bash
deno upgrade
```
Use the original package manager to upgrade Homebrew, Scoop, or other managed installations.
## Editor
Install the official Deno VS Code extension and run `Deno: Initialize Workspace Configuration`. In mixed Node/Deno monorepos, enable it only for Deno folders so two TypeScript language servers do not diagnose the same source.
Acceptance: `deno --version` succeeds, the editor recognizes `Deno.serve`, and the terminal and editor resolve the same Deno binary.
Official references: [Installation](https://docs.deno.com/runtime/getting_started/installation/) and [Set up your environment](https://docs.deno.com/runtime/getting_started/setup_your_environment/).
---
# Mental model
Source: /en/docs/getting-started/mental-model/
## Deno in one diagram
```text
source (.ts/.js/.wasm)
├─ modules: local / JSR / npm / URL
├─ config: deno.json(c) + deno.lock
├─ tools: fmt / lint / check / test / doc / compile
└─ runtime: Web APIs + Deno APIs + Node compatibility
↓
permission boundary (--allow-* / --deny-*)
↓
files, network, environment, subprocesses, FFI
```
## Three important differences from Node.js
1. **Secure defaults:** sensitive I/O is denied until granted, and grants can be scoped to paths, hosts, or variable names.
2. **Direct TypeScript execution:** execution transpiles TypeScript but does not imply a full type-check on every run; keep `deno check` in CI.
3. **One toolchain:** formatting, linting, testing, and docs ship with the same CLI version.
## Dependencies have multiple sources
- JSR packages use `jsr:` and work well for TypeScript-native packages and the Deno standard library.
- npm packages use `npm:`; Node built-ins use `node:`.
- `imports` in `deno.json` maps full specifiers to stable bare import names.
- `deno.lock` pins resolution and integrity data and should be committed.
“Runs `.ts`” does not mean “performs a full type-check before every execution.” Put `deno check` in CI to turn type errors into a release gate.
Continue with [Permissions](/en/docs/core/permissions) and [Dependencies](/en/docs/core/dependencies).
---
# 5-minute quickstart
Source: /en/docs/getting-started/quickstart/
## 1. Install and verify
```bash
curl -fsSL https://deno.land/install.sh | sh
deno --version
```
```powershell
irm https://deno.land/install.ps1 | iex
deno --version
```
```bash
docker run --rm denoland/deno:latest deno --version
```
## 2. Initialize and run
```bash
deno init --serve hello-deno
cd hello-deno
deno task dev
```
`deno init --serve` creates `deno.json`, a server entry point, and tests. For a hand-written entry point, make the network permission explicit:
```ts title="main.ts"
Deno.serve((_request) => Response.json({ ok: true }));
```
```bash
deno run --allow-net main.ts
curl http://localhost:8000
# {"ok":true}
```
## 3. Add a test
```ts title="math_test.ts"
import { assertEquals } from "jsr:@std/assert";
Deno.test("adds two numbers", () => {
assertEquals(2 + 3, 5);
});
```
```bash
deno test
```
## 4. Pre-commit gate
```bash
deno fmt --check
deno lint
deno check **/*.ts
deno test
```
`-A` means `--allow-all` and disables the sandbox. A server that only listens needs `--allow-net`; a process reading one secret should use `--allow-env=KEY_NAME`.
Official sources: [Get started](https://docs.deno.com/runtime/), [Installation](https://docs.deno.com/runtime/getting_started/installation/), and [Testing](https://docs.deno.com/runtime/test/).
---
# What is Deno?
Source: /en/docs/getting-started/what-is-deno/
Deno is an open-source JavaScript, TypeScript, and WebAssembly runtime started by Node.js creator Ryan Dahl. It does not invent another language; it brings modern Web APIs, secure permissions, package management, and engineering tools into one CLI.
## Why Deno exists
Node.js predates modern ESM and today's front-end ecosystem. Deno chose ES Modules, URL or registry dependencies, Web-standard APIs, and default isolation, then built formatting, linting, testing, and type checking into the runtime. Deno 2 added stronger Node and npm compatibility, so migration can now be incremental.
```text
Node.js project Deno-first project
├── package.json ├── deno.json
├── package-lock.json ├── deno.lock
├── node_modules ├── global cache by default
├── tsc / eslint / test runner └── deno fmt / lint / check / test
└── ambient system access └── sensitive access denied by default
```
## Which runtime fits?
| Workload | Natural starting point | Verify first |
| --- | --- | --- |
| Established Node packages and workflows | Node.js or incremental Deno | native addons, loaders, lifecycle scripts |
| TypeScript APIs, CLIs, and automation | Deno | permission inventory and npm compatibility |
| Bun-specific tooling and startup profile | Bun | Node API and hosting support |
| Cloudflare bindings and global edge | Workers | Node subset, CPU, and platform limits |
| Fresh, Deno Deploy, or MCP tool services | Deno | current framework and platform versions |
Production projects should still commit `deno.json`, `deno.lock`, tasks, and least-privilege commands. Strong defaults do not replace engineering constraints.
Official references: [Deno runtime](https://docs.deno.com/runtime/), [Node and npm compatibility](https://docs.deno.com/runtime/fundamentals/node/), and [Cloudflare Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/).
---
# Migrate from Node.js to Deno
Source: /en/docs/migration/
Deno 2 reads `package.json`, resolves npm packages, and runs a broad set of Node APIs. Migration should not begin by rewriting imports. It should begin by proving that the application still satisfies its tests and production constraints on another runtime.
## Four-stage route
## Acceptance at every stage
```bash
deno install
deno check src/main.ts
deno test
deno task start
```
Do not remove the old lockfile, runtime command, or deployment path until CI, developer machines, and the target production environment all pass. Change only one of runtime, package management, testing, framework, or deployment in a single migration step.
If a critical dependency only ships an incompatible Node-API binary, the system deeply depends on custom loaders, or no production regression suite exists, build evidence and isolation first. Do not rewrite stable business logic just to declare the migration complete.
Official references: [Migrate to Deno](https://docs.deno.com/runtime/migrate/) and [Node and npm compatibility](https://docs.deno.com/runtime/fundamentals/node/).
---
# Node API mapping
Source: /en/docs/migration/api-mapping/
Deno supports a broad set of `node:` APIs, so migration does not require rewriting every call. Start with compatibility APIs, then adopt Web or Deno APIs where they reduce dependencies or improve portability.
| Node.js | Deno / Web API | Guidance |
| --- | --- | --- |
| `fs.promises.readFile(path, "utf8")` | `Deno.readTextFile(path)` | both need file permission |
| `fs.promises.writeFile` | `Deno.writeTextFile` | scope access to a directory |
| `http.createServer` | `Deno.serve` | prefer Web Request/Response for new servers |
| `process.env.NAME` | `Deno.env.get("NAME")` | grant only named variables |
| `process.argv.slice(2)` | `Deno.args` | direct CLI equivalent |
| `child_process.spawn` | `new Deno.Command()` | needs `--allow-run=` |
| `__dirname` | `new URL(".", import.meta.url)` | keep URL semantics, convert only if needed |
| `crypto.randomUUID()` | `crypto.randomUUID()` | reuse the Web API |
| `Buffer` | `Uint8Array` / `TextEncoder` | prefer Web types at protocol boundaries |
## File-reading example
```ts
// Compatibility first: keep the Node API
import { readFile } from "node:fs/promises";
const a = await readFile("config.json", "utf8");
// Deno-first: use the shorter text API
const b = await Deno.readTextFile("config.json");
```
Both should run with the same least privilege:
```bash
deno run --allow-read=config.json main.ts
```
Migration tests should cover paths, encoding, stream backpressure, signals, timeouts, and error shapes. Similar names do not guarantee identical boundary behavior.
Official references: [Node APIs](https://docs.deno.com/api/node/), [Deno APIs](https://docs.deno.com/api/deno/), and [Web APIs](https://docs.deno.com/api/web/).
---
# Migrate from Node.js
Source: /en/docs/migration/from-node/
Deno 2 can run many existing Node projects. The goal is not to rewrite every import at once; it is to accumulate reversible compatibility evidence.
## Incremental adoption
1. Pin Deno and record `deno --version` in the existing branch.
2. Keep `package.json`, the current lockfile, and the existing `node_modules` strategy; run one side-effect-free script first.
3. Execute current tests and classify failures in Node APIs, native addons, loaders, and lifecycle scripts.
4. Introduce `deno.json` tasks and run Deno commands beside current commands.
5. Remove old tools or lockfiles only after CI, developer, and production verification.
## Compatibility conventions
```ts
import { readFile } from "node:fs/promises";
import express from "npm:express";
```
Deno can also resolve common bare npm imports from `package.json`. Whether a local `node_modules` is enabled or required depends on project configuration and dependency behavior; one mode does not fit every package.
## High-risk areas
- Node-API addons, postinstall scripts, and binary downloads;
- custom ESM loaders, resolution hacks, and implicit extensions;
- Jest/Vitest globals, fake timers, and snapshot differences;
- undeclared environment, filesystem, and network access;
- edge behavior in `process`, Buffer, streams, and Node APIs.
Do not change runtime, test framework, package manager, formatting rules, and deployment platform in one migration commit. Failures become impossible to attribute.
Official references: [Node compatibility](https://docs.deno.com/runtime/fundamentals/node/) and [Migrate to Deno](https://docs.deno.com/runtime/migrate/).
---
# npm, package.json, and deno.json
Source: /en/docs/migration/packages-and-config/
Deno 2 can read `package.json` and `deno.json` together. Do not delete Node configuration at the start of a migration; first make the existing project install, check, and test under Deno.
| Node workflow | Deno counterpart |
| --- | --- |
| `npm install` | `deno install` |
| `npm install lodash` | `deno install lodash` |
| `npm run dev` | `deno task dev` (package scripts also work) |
| `package-lock.json` | `deno.lock`; both may coexist during migration |
| bare `lodash` import | resolved in package.json projects; Deno-first code may use `npm:lodash` |
## Three node_modules modes
| Mode | Use case |
| --- | --- |
| `none` | new Deno projects using the global cache |
| `auto` | bundlers, Node-API, or tools require a local directory |
| `manual` | existing package.json workflows with an explicit install step |
```json
{
"nodeModulesDir": "auto",
"tasks": {
"dev": "deno run --watch -N -E src/main.ts",
"verify": "deno fmt --check && deno lint && deno check src/main.ts && deno test"
}
}
```
Lifecycle scripts do not run unconditionally. When a dependency genuinely needs one, run `deno approve-scripts` to review and approve interactively (approvals persist to `allowScripts` in deno.json — the recommended flow since Deno 2.6); `deno install --allow-scripts=` remains available for one-off approvals. Either way, re-audit whenever the lockfile changes.
Removing the npm lockfile and node_modules is the end of a migration, not the beginning. Keep a working rollback path until a Deno-first layout is proven.
Official references: [Dependency management](https://docs.deno.com/runtime/packages/) and [Node and npm compatibility](https://docs.deno.com/runtime/fundamentals/node/).
---
# Node migration troubleshooting
Source: /en/docs/migration/troubleshooting/
## Cannot find module
Run `deno install` first. If the package is declared in `package.json` but a tool needs a physical directory, choose `nodeModulesDir: "auto"` or retain manual mode. Do not copy dependencies by hand.
```bash
deno info src/main.ts
deno check src/main.ts
```
## require is not defined
Prefer converting your own code to ESM. When CommonJS must remain, use `.cjs` or set `"type": "commonjs"` in the nearest `package.json`. Do not hide the boundary with a fake global `require`.
## An npm package resolves but fails at runtime
Check, in order:
1. Node-API native addons;
2. required install or postinstall scripts;
3. assumptions about a writable `node_modules`;
4. ungranted environment, certificate, or config-file access;
5. Node API behavior that is not yet compatible.
## Tests pass on Node but fail on Deno
Keep the original runner at first and execute it under Deno; do not migrate to `Deno.test` in the same step. Inspect globals, fake timers, snapshot paths, environment variables, and temporary directories. Convert tests only after a compatibility baseline exists.
## PermissionDenied
This is evidence of missing access, not a reason to add `-A`. Use `DENO_TRACE_PERMISSIONS=1` or permission auditing to identify the resource, then grant only the host, variable, command, or directory involved.
See [common errors](/en/docs/reference/errors) for runtime issues. Official references: [Node compatibility](https://docs.deno.com/runtime/fundamentals/node/) and [Migrating from Node](https://docs.deno.com/runtime/migrate/).
---
# Deno project blueprints
Source: /en/docs/projects/
These are more than Hello World demos. Each blueprint can be implemented, tested, and deployed in vertical slices. Finish one end-to-end slice before adding features.
## 1. [Fresh blog](/en/docs/projects/fresh-blog)
```text
fresh-blog/
├── routes/posts/[slug].tsx
├── routes/admin/posts.tsx
├── islands/PostEditor.tsx
├── components/
├── src/db/{client,schema}.ts
└── deno.json
```
Stack: Fresh 2, PostgreSQL, and Drizzle. Phase one covers a post list, detail page, and protected create endpoint. Markdown, drafts, images, and search belong in later slices.
Acceptance: posts remain readable without JavaScript, the slug constraint works, only the editor island hydrates, and migrations run as a separate release step.
## 2. [Deno MCP server](/en/docs/projects/mcp-server-project)
```text
mcp-project/
├── main.ts
├── tools/{search,read}.ts
├── schemas.ts
├── fixtures/
└── deno.json
```
Begin with stdio and read-only tools. Validate arguments with Zod and protocol-test fixtures before adding Streamable HTTP, authentication, or writes. Follow the [MCP server guide](/en/docs/ai/mcp-server).
## 3. [Cron service](/en/docs/projects/cron-service)
```text
cron-service/
├── jobs/daily-report.ts
├── services/report.ts
├── main.ts
└── deno.json
```
Write the job as a normal testable function, then trigger it with `Deno.cron()`. Make it idempotent with a database constraint or execution key, and expose delayed, failed, and replayed runs to observability.
## 4. [Image-processing API](/en/docs/projects/image-api)
```text
image-api/
├── routes/transform.ts
├── services/image.ts
├── storage.ts
└── main.ts
```
Use Hono plus `npm:sharp` to accept size-limited images, validate MIME and pixel dimensions, transform, and write to object storage. `sharp` includes native code, so smoke-test the target Deno version and container or Deploy environment before committing to it.
## Shared definition of done
- `deno fmt --check && deno lint && deno check && deno test` passes;
- `deno.lock` is committed and install scripts have an explicit allowlist;
- development, migration, tests, and production use separate least-privilege commands;
- health checks, structured logs, timeouts, error mapping, and secret management are wired;
- README documents local startup, database setup, deployment, and rollback.
---
# "Project: Deno cron service"
Source: /en/docs/projects/cron-service/
This tutorial expands [blueprint 3](/en/docs/projects) into concrete steps. For Deno Deploy's timeline, database, and cron platform behavior see [Databases, cron, and timelines](/en/docs/deploy/data-and-cron); this page focuses on the service itself.
## Goal and final shape
```text
cron-service/
├── jobs/daily-report.ts # scheduling entrypoint: claim the execution key, call the service, record the outcome
├── services/report.ts # pure business logic, unaware of cron
├── main.ts # Deno.cron registration (module top level)
├── migrations/ # the job_runs table
└── deno.json
```
Core discipline: **no business logic inside the `Deno.cron()` handler**. A job is a plain async function that tests call directly; cron only triggers it on UTC time.
## Setup
```bash
deno init cron-service
cd cron-service
deno add npm:postgres
```
The example stores execution records in PostgreSQL (the `postgres` driver is pure JavaScript and works directly in Deno). The connection string lives in the `DATABASE_URL` environment variable. With Deno KV instead, the idea is the same — claim a unique key with an atomic operation.
## Milestone 1: a testable job
### Business logic independent of scheduling
```ts title="services/report.ts"
import type { Sql } from "postgres";
export async function buildDailyReport(sql: Sql, day: string) {
const rows = await sql`
select count(*)::int as orders, coalesce(sum(total), 0)::numeric as revenue
from orders
where created_at >= ${day}::date
and created_at < (${day}::date + interval '1 day')`;
return { day, orders: rows[0].orders, revenue: rows[0].revenue };
}
```
`day` is an explicit parameter, so tests can replay any date without waiting for a real schedule.
### Idempotency: a unique execution key
The core problem: cron may retry, and a manual replay re-runs the same logical date. Register every execution in a table and "claim" it with a unique `(job_name, execution_key)` constraint:
```sql title="migrations/0001_job_runs.sql"
create table job_runs (
id serial primary key,
job_name text not null,
execution_key text not null,
status text not null default 'running',
started_at timestamptz not null default now(),
finished_at timestamptz,
error text,
unique (job_name, execution_key)
);
```
```ts title="jobs/daily-report.ts"
import type { Sql } from "postgres";
import { buildDailyReport } from "../services/report.ts";
export async function runDailyReport(sql: Sql, day: string) {
const claimed = await sql`
insert into job_runs (job_name, execution_key)
values ('daily-report', ${day})
on conflict (job_name, execution_key) do update
set status = 'running', started_at = now(),
finished_at = null, error = null
where job_runs.status = 'failed'
returning id`;
if (claimed.length === 0) {
return { skipped: true, day }; // already succeeded or currently running; return immediately
}
try {
const report = await buildDailyReport(sql, day);
// Business writes use day as the idempotency key too: upsert
await sql`
insert into daily_reports (day, orders, revenue)
values (${day}, ${report.orders}, ${report.revenue})
on conflict (day) do update
set orders = excluded.orders, revenue = excluded.revenue`;
await sql`
update job_runs set status = 'ok', finished_at = now()
where id = ${claimed[0].id}`;
return report;
} catch (err) {
await sql`
update job_runs set status = 'failed', finished_at = now(),
error = ${String(err)}
where id = ${claimed[0].id}`;
throw err;
}
}
```
Both idempotency layers are required: `job_runs` prevents duplicate executions, and the `daily_reports` upsert guarantees that even if a retry lands after the business write but before the status update, a rerun only overwrites the same row.
Note the `do update ... where status = 'failed'` in the claim statement: with `on conflict do nothing`, a failed record would hold the unique key forever, and both platform retries and manual replays would be skipped as "already executed." Another edge is a permanent `running` row left behind when the process crashes before the status update — in production, add a reclamation rule that treats a `started_at` older than a threshold as an expired lease.
## Milestone 2: register the cron job
```ts title="main.ts"
import postgres from "postgres";
import { runDailyReport } from "./jobs/daily-report.ts";
const sql = postgres(Deno.env.get("DATABASE_URL")!);
function utcYesterday(): string {
const d = new Date(Date.now() - 86_400_000);
return d.toISOString().slice(0, 10);
}
Deno.cron(
"daily-report",
"0 3 * * *",
{ backoffSchedule: [60_000, 300_000, 900_000] },
async () => {
console.log(JSON.stringify({ job: "daily-report", day: utcYesterday() }));
await runDailyReport(sql, utcYesterday());
},
);
```
Key points (platform details in the [Deploy docs](/en/docs/deploy/data-and-cron)):
- Schedules are **UTC**; `0 3 * * *` means 03:00 UTC, not local time.
- **Failed executions are not retried by default.** Opt in with an explicit `backoffSchedule`: each array element is the millisecond delay before the next retry, with at most 5 retries and a 1-hour ceiling per delay.
- The same job never runs concurrently: if the previous execution is still going (or a retry overlaps the next scheduled run), the later one is skipped. Long-running jobs do not pile up.
- Registration must happen at **module top level**, before the server starts; jobs declared inside request handlers or conditionals are not discovered by Deploy.
## Test strategy
Don't test by waiting for the real schedule locally — call the function directly:
```ts title="jobs/daily-report_test.ts"
import { assertEquals } from "jsr:@std/assert";
import postgres from "postgres";
import { runDailyReport } from "./daily-report.ts";
const sql = postgres(Deno.env.get("TEST_DATABASE_URL")!);
Deno.test("re-running the same day executes only once", async () => {
// Clean the execution key first, so stale rows in the test database can't make the case pass vacuously
await sql`delete from job_runs where job_name = 'daily-report' and execution_key = '2026-08-01'`;
await sql`delete from daily_reports where day = '2026-08-01'`;
const first = await runDailyReport(sql, "2026-08-01");
const second = await runDailyReport(sql, "2026-08-01");
assertEquals((first as { skipped?: boolean }).skipped, undefined);
assertEquals((second as { skipped?: boolean }).skipped, true);
const rows = await sql`
select count(*)::int as n from daily_reports where day = '2026-08-01'`;
assertEquals(rows[0].n, 1);
});
```
Add two more cases: when the business write fails midway, `job_runs` records `failed` and a replay can recover; after replaying, `daily_reports` still holds exactly one row. Use a dedicated `TEST_DATABASE_URL`, separate from the development database.
## Deployment and observability
- Deploy discovers `Deno.cron()` definitions by evaluating top-level module code at deploy time, then takes over scheduling; rolling back a revision re-registers that revision's jobs. Production and each Git branch timeline fire independently — previews run jobs too, so tag logs with `DENO_TIMELINE` and skip external side effects outside production when needed.
- Executions appear in the dashboard's Cron tab and in logs/traces (filter by `kind:cron`, `cron.name:`); the `job_runs` table answers the business questions: which day never ran, which run failed, what the replay produced.
- Each cron execution is billed as one inbound HTTP request; free organizations are limited to 10 cron jobs per revision.
- Permissions: the service needs `--allow-net` (database) and `--allow-env=DATABASE_URL` — never `-A`.
## Acceptance checklist
Official references: [Deno Deploy Cron](https://docs.deno.com/deploy/reference/cron/), [Timelines](https://docs.deno.com/deploy/reference/timelines/), [postgres.js](https://github.com/porsager/postgres).
---
# "Project: Fresh 2 + PostgreSQL + Drizzle blog"
Source: /en/docs/projects/fresh-blog/
This tutorial expands [blueprint 1](/en/docs/projects) into concrete steps. For routing, islands, and middleware syntax see [Fresh 2 full-stack development](/en/docs/web/fresh); this page covers only what the blog adds.
## Goal and final shape
```text
fresh-blog/
├── routes/posts/[slug].tsx # post detail (server-rendered)
├── routes/index.tsx # post list
├── routes/admin/posts.tsx # protected create page + POST
├── routes/admin/_middleware.ts # access control for the admin area
├── routes/admin/login.tsx # login form (sets a cookie)
├── islands/PostEditor.tsx # the only component that hydrates
├── components/
├── src/db/{client,schema}.ts # Drizzle connection and tables
├── drizzle.config.ts
└── deno.json
```
Phase one ships the list, detail pages, and a protected create flow. Phase two adds Markdown rendering, drafts, images, and search. The create form uses a native `