Deno's development is ending, so I moved my SaaS starter to Node and Bun
Deno's team is joining Cloudflare, and the runtime's development ends after one more year of fixes. Seed, my SaaS starter, ran on Deno and Fresh, so I moved it to run on Deno, Node and Bun. This post is the honest account: what was hard, what wasn't, and the slowdown I shipped on the way.
TL;DR
- The Deno.* calls were not the hard part. Serving files, Deno types in public signatures and Fresh's internals were.
- I replaced Fresh by copying its API: 419 of 423 responses came out identical, and every page shipped less JavaScript.
- My first release of that layer made the slider post block input for 119 ms against Fresh's 51.5 ms. CI missed it: none of its pages has an island.
- Don't rewrite. Put each Deno.* behind a small adapter, run the build on Node and Bun in CI, and diff every response before and after.
Seed: Live: Open the live demo · Code: spy4x/seed
To migrate from Deno to Node and Bun, you don't rewrite your app. You keep it on
web standards (fetch, Request, Response), put every Deno.* call behind a
small adapter, and replace what only runs on Deno, such as Fresh. I did exactly
that for [Seed](/blog/seed-saas-starter), the SaaS starter every new project of
mine starts from. The pull requests that finished it merged on 4 November 2026.
This post is for a developer or a tech lead with a Deno codebase who read the
news and now has to decide: stay, fork or move. Short version of the worry "my
code is full of Deno.*": those calls were the easy part. It shows what was
really tied to Deno in a tested codebase, what replaced each piece, and the
mistake I shipped on the way.
What happened to Deno?
On 9 October 2026, Ryan Dahl wrote that
Deno is joining Cloudflare. The runtime gets
another year of monthly releases with bug fixes and security updates, and then:
"After that year we will end our development of the Deno runtime." Deno stays
open source, and others are welcome to continue it.
The same post says Deno Deploy keeps running for six months and then shuts down,
and JSR keeps running on Cloudflare's infrastructure.
Cloudflare's own post says
the team will merge Deno's celld into Cloudflare's workerd runtime. Neither
post says what happens to Fresh, Deno's web framework.
So nothing breaks tomorrow. Your app runs the same today as it did yesterday.
What changes is the future: a bug or a security hole found after that year may
never be fixed.

Why move a starter that worked?
Seed is not one app. It is the starting point of every product I build, client
MVPs included. A runtime that stops getting fixes under it would sit under all
of them.
So the goal was not "leave Deno". It was "run unchanged on Deno, Node and Bun".
Deno stays both a runtime Seed supports and its toolchain: deno fmt,
deno lint and deno test still run every check. If Deno finds a new home,
nothing is lost. If it doesn't, the switch is a start file, not a rewrite.
"But my code is full of Deno.*"
That is the usual worry, and it was mine too. Here is the honest answer: the
Deno.* calls were the easy part.
Reading an environment variable, serving a request, opening a file: each has an
obvious twin on Node, and Bun implements Node's node:fs and node:net
(Bun's compatibility list). So one
small adapter covers both. This is the start file that runs Seed's website on
Node and Bun, trimmed of its port check. Deno runs the same built server with
deno serve:
import { serve, type ServeHandler } from "@spy4x/platform/server/serve"
import { shutdownSignal } from "@spy4x/platform/server/shutdown-signal"
import { exitProcess, processEnv } from "@spy4x/platform/server/process"
const hostname = processEnv.get(`MPA_HOST`) || `0.0.0.0`
const port = Number(processEnv.get(`MPA_PORT`) || 8000)
const built: { default: { fetch: ServeHandler } } = await import(
new URL(`./server.js`, import.meta.url).href
)
const server = await serve(built.default.fetch, {
hostname,
port,
signal: shutdownSignal(),
})
await server.finished
exitProcess(0)
1. processEnv reads Deno.env on Deno and process.env on Node and Bun.
2. The built server is a plain fetch handler: a Request in, a Response
out. That is the web standard doing the heavy lifting.
3. serve() listens on whichever runtime it finds itself on.
4. shutdownSignal() is an AbortSignal that fires on SIGINT or SIGTERM, so
requests in flight finish before the process exits.
Even the Redis client needed nothing. It already talked over a connectTcp
adapter in my shared library: Deno.connect on Deno, node:net on Node and
Bun. I tested it on Valkey 8.1, Node 26.13 and Bun 1.4.4.
So what was hard?
Four things, and only one of them is a function call:
1. Default arguments that call Deno.*. A function with a default like
fs = denoFs looks harmless until Node imports it, and there is no Deno to
call. The defaults now pick the running runtime's version.
2. Deno types in exported signatures. Deno.Signal sat in public types, so
a Node project could not even type-check my library. It became OsSignal, a
list of the same 36 names, with a type test in both directions so the two can
never drift apart.
3. Error classes. Deno throws Deno.errors.IsADirectory; Node throws an
error with code: "EISDIR". One fsError and isFsError pair maps both.
The Node adapter is imported lazily, so deno check doesn't pull in Node's
types.
4. Serving files. Range requests and ETags took the most care. A test caught
bytes=10- on a 10-byte file returning 200 instead of 416: a range that
starts past the end of the file asks for nothing, and the right answer is
"Range Not Satisfiable". A reviewer caught that a weak ETag must never
satisfy If-Range, because the
HTTP standard
compares it with the strong comparison only.
The final check was blunt: deno bundle of the 77 @spy4x/* imports Seed uses
produced one .mjs file, and it ran directly on Node and Bun, with no
package.json. All of that shipped as release 1.116.0 of my shared library.
How do you replace Fresh without rewriting every route?
Fresh was the real tie. It is
Deno's own framework, built by the same
company, and I had built Seed's public website on it. That is the mistake I own
here: I put a framework owned by the runtime's company under the one project
everything else starts from.
I didn't rewrite the routes. I wrote a small layer that copies Fresh 2's API:
App, ctx.render, createDefine, fsRoutes. It renders with Preact, builds
with Vite and runs on web standards. It is
@spy4x/preact-server,
1,459 lines of source and 919 lines of tests, part of my open-source component
library. Because the API matched, Seed's route files barely changed. Seed's side
of the switch, spy4x/seed#815, touched
128 files and deleted more than it added: +562, −908.
Did anything change for the visitor?
To find out, I requested every route on both builds and compared the answers
(spy4x/seed#623). 419 of 423
responses were identical. Two of the four differences were one video's
modification time and ETag. The other two were the malformed path /%E0%A4%A:
Fresh answered with a bare 500, the new layer answers with the site's 404 page.
The pages also got lighter. Gzipped JavaScript per page, Lighthouse mobile, 22
pages, median of 3 runs:
| Page | New layer | Fresh |
| ----------------------- | --------- | ------- |
| Home | 24.0 KB | 32.1 KB |
| A blog post with island | 29.9 KB | 38.0 KB |
| Docs | 8.6 KB | 19.4 KB |
Every page scored 98–100 on both builds. Of the 112 end-to-end tests, 110 passed
on the first run. The two that failed tested Fresh's internals: an island's file
name and the CSP nonce.
What's better, and what's missing?
Two things got better:
- No nonce in the CSP. The layer puts island props in a JSON block, not an
inline script, so the Content-Security-Policy needs no nonce. This very site
still runs on Fresh, and its CSP code reads Fresh's private nonce. That is
exactly the kind of internal I don't want to depend on.
- Safer redirects. ctx.redirect() keeps /\host and /<tab>/host on the
site
(the test).
Fresh lets both through to another host.
And four things Fresh has that the layer doesn't: csrf() middleware (a
same-origin check from my shared library does the job), _404 and _500
routes, its own dev server (Vite runs development instead), and a per-route
opt-out of the modulepreload header.
The scar: my replacement made the slider post slower than Fresh
Seed's blog has a post with an interactive slider,
the welcome post. On the
first release of my layer, 13.11.0, that page blocked input for 119 ms. Fresh
blocked it for 51.5 ms. That is Lighthouse 12.8.2, mobile, 6 alternating runs
each. The page shipped less JavaScript and still blocked input more than twice
as long.
The cause: loading and hydrating every island ran as one long task. The browser
could not answer a tap until the last island was awake.

The fix, in 13.11.1
(#1193), gives the
browser a turn before each island:
function yieldToMain(): Promise<void> {
const scheduler = (globalThis as {
scheduler?: { yield?: () => Promise<void> }
}).scheduler
return scheduler?.yield
? scheduler.yield()
: new Promise((resolve) => setTimeout(resolve, 0))
}
It uses
scheduler.yield()
where the browser has it and falls back to setTimeout(0). The blocking time
dropped to 2 ms. The design reviewer re-checked it over 7 runs: 5 ms against
Fresh's 48 ms, and a Lighthouse score of 97 against 96.
Why didn't CI catch it? Seed's CI does run a Lighthouse speed check. But none of
the pages it measures has an island
(spy4x/seed#816). A speed check only
guards the pages you give it. If your most expensive page isn't in the list, its
regression ships green.
> Measure the page with the heaviest island, not the page that's easiest to
> measure.
The trap: one line in package.json broke the tests
The last pull request, spy4x/seed#817,
added the Node and Bun start files, their Docker images and a CI smoke test that
builds the site and runs it on all three runtimes. 27 files, +768, −212.
Two surprises came with it. Adding "type": "module" to the root package.json
broke Playwright under Deno. The fix was to leave the field out: that file only
hands npm run and bun run over to deno task. And in CI, the new smoke test
and the browser tests raced each other on vapid.json and the build folders.
The fix: the smoke test now runs in its own copy of the checkout.

How did AI agents split the work?
I didn't type most of this code. One coordinator session, an AI agent, started
separate sessions for each lane: my shared library, the website layer in my
component library, the Seed port, and landing the move off Fresh. Each worked in
its own git worktree with its own pull request.
An independent reviewer agent gated every merge. On changes a visitor can see, a
design reviewer gated it too, in a real browser. They earned their place: the
weak-ETag If-Range bug was a reviewer's find, and the 13.11.0 slowdown was the
design reviewer's measurement.
One honest lesson. When a session stopped halfway, the coordinator first only
passed its report on to me instead of carrying the work on. I had to point that
out. Now a coordinator continues a stopped session's work itself.

An agent also drafted this post. Every number in it comes from a merged pull
request or from the measurements in
spy4x/seed#623.
How tied to Deno is your project?
Here is every tie I found, and what replaced it in Seed. The first eleven were
Seed's; the last three Seed never had:
| Your project uses | What replaced it in Seed |
| ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Deno.serve or deno serve | The servers export a standard fetch handler. Deno still runs it with deno serve; on Node and Bun a start file hands it to serve() from @spy4x/platform. |
| Deno.env | processEnv from @spy4x/platform: Deno.env on Deno, process.env on Node and Bun. |
| File APIs: Deno.readFile, Deno.stat, Deno.open | A small file interface with two versions: Deno.* on Deno, node:fs/promises on Node and Bun. |
| Deno's error classes, such as Deno.errors.NotFound | One fsError and isFsError pair that also knows Node's code: "EISDIR". |
| Deno.Signal or other Deno types in exported signatures | OsSignal, a list of the same 36 names, with a type test in both directions. |
| Default arguments that call Deno.* | Defaults that pick the running runtime's version. |
| Deno.connect, such as a Redis client | Nothing: the client already ran over a connectTcp adapter, Deno.connect on Deno and node:net on Node and Bun. |
| Fresh | @spy4x/preact-server, a new layer that copies Fresh 2's API. |
| jsr: imports | deno bundle builds them into the server file, so Node and Bun never see a jsr: import. |
| Deno.Command | Nothing: only the dev, deploy and media scripts use it, and they stay on Deno. |
| deno test, deno fmt, deno lint | Nothing: the toolchain stays Deno. npm run and bun run call the same deno tasks. |
| Deno KV | Never used: the data lives in Postgres and Valkey. |
| Deno Deploy | Never used: Seed runs in Docker Compose on any Linux host. |
| deno compile | Never used: Seed ships Docker images. |
If you ticked Deno Deploy, start there: it has six months, not a year. If you
ship single binaries with deno compile, as I do for
[my MCP servers](/blog/building-mcp-servers-with-deno), that is a decision of
its own. Tick your own project's ties below:
The table above also works as a checklist you can tick for your own project.
Try it on antonshubin.com: <antonshubin.com>
Should you stay, fork or move?
My answer: don't panic, and don't rewrite. You have a year of fixes. Use it to
make the switch cheap, then decide when you have to:
1. Count your ties. The table above is a start. Look hardest at your public
types and your default arguments: that is where Deno hides.
2. Put each Deno.* behind a small adapter. One function per need, with a
Deno version and a Node version. Bun takes the Node one.
3. Run the build on Node and Bun in CI. A smoke test that starts the server
on each runtime and requests a page catches the rest.
4. Replace the framework last, by copying its API. Then diff every response
before and after, so "it works" is a number, not a feeling.
5. Measure your heaviest page. The one with the most islands, not the one
that's easiest to measure.
If your product runs on Deno and you'd like a calm plan instead of a panicked
rewrite, that is what my
[code and security audit](/catalog/codebase-health-audit) is for: three days,
from $1,500. It covers architecture, security and performance, and for a Deno
codebase that includes every tie to Deno, ranked by risk, with what to move
first and what to leave alone.
And if you've already started your own move, tell me what bit you. I'd like to
know if it was the Deno.* calls or, like me, everything around them.
---
_First published on antonshubin.com._
Deno Deploy Is Shutting Down: Where to Move Your App, and How Big the Move Is
The usual questions about where to run TypeScript functions are latency and cost. Deno Deploy users now have a blunter one: the platform is going away. On October 9, 2026, Ryan Dahl said the whole Deno team is moving to Cloudflare and that Deploy will shut down six months later, which puts the deadline around April 2027.
I spent a day sizing the migration on a real codebase and pricing the main destinations. This is the condensed version; the complete guide on DevToolLab has the audit script, every pricing line and screenshots of each platform.
What Ends and What Survives
Per Deno's announcement, Deploy runs for six more months and then closes. Deno Land will keep shipping runtime patches, bug and security fixes once a month, for a year and then stop working on the runtime; the code stays open source, MIT licensed as it is today. JSR keeps operating while its infrastructure moves to Cloudflare, and rusty_v8 stays supported while the team plans to fold it into workerd.
Paying customers get migration help, but the announcement only mentions moving to Cloudflare Workers. Deno Sandbox and Subhosting share Deploy's plans ($0 Free, $20 Pro, $200 Builder), and nobody has said what happens to them.
Most of the Pain Lives in a Few Files
Five kinds of Deno-only code make a Deploy app hard to move: KV reads and writes, KV queues, Deno.cron jobs, Deno.* file access and jsr: package imports. I wrote a small Node script that counts those patterns in a project, then ran it against SaaSKit, Deno's own Fresh SaaS starter built for Deploy (now archived, last commit February 9, 2026):
69 source files scanned
Deno.openKv() 1 hits in 1 files
KV atomic / watch / listenQueue 5 hits in 1 files
Deno.cron 0 hits in 0 files
BroadcastChannel 0 hits in 0 files
Deno.serve 0 hits in 0 files
Deno.env 5 hits in 3 files
Deno filesystem calls 5 hits in 2 files
jsr: imports 10 hits in 2 files
npm: imports 8 hits in 2 files
https:// URL imports 1 hits in 1 files
The interesting part is concentration. All of SaaSKit's KV usage sits in utils/db.ts, a 483-line module with 15 KV calls that 22 other files import. Swapping storage means rewriting that one module, not the app. The script itself is about 40 lines and is in the original post if you want to point it at your own repo.
Seven Places to Go
The worked example throughout: a SaaSKit-shaped app with an AI chat endpoint, KV for users and sessions, and roughly 3 million requests a month.
| Platform | Deno code unchanged? | KV replacement | Example month |
|---|---|---|---|
| Cloudflare Workers | No | Workers KV, D1, Durable Objects | $5 |
| Supabase Edge Functions | Mostly | Postgres | $27 |
| workerd + celld | No | Durable Objects | your servers |
| Netlify Edge Functions | Mostly | Netlify Blobs | $9 |
| Fly.io | Yes, it runs Deno | self-hosted denokv | about $4 |
| Val Town | Mostly | per-val SQLite | $25 |
| Vercel Functions | Community runtime only | Blob or a database | $20 and up |
Cloudflare Workers
The Deno engineers now work on this platform, and it is the one target the migration offer names. Durable Objects give each object its own SQLite database plus WebSockets, a good fit for agent-style workloads, and Deno.cron maps to Cron Triggers.
!Cloudflare Workers homepage
There is no Deno global here, so SaaSKit's database module needs rewriting, as does each Deno.env lookup. The Paid plan is $5 a month with 10M requests and 30M CPU milliseconds included; at 5 ms of CPU per request and KV inside its included reads and writes, the example stays at $5. The free plan caps CPU at 10 ms per invocation.
Supabase Edge Functions
Supabase's Edge Runtime is Deno-compatible and MIT licensed, so most handler code moves with light edits, and Postgres replaces KV with pg_cron for schedules. Each hosted invocation is limited to 2 seconds of CPU time and 256 MB of RAM. Pro is $25 a month with 2M invocations, then $2 per million, so the example lands at $27.
workerd and celld, Self-Hosted
workerd is the Apache 2.0 runtime under Workers, and celld is Deno's self-hosted Durable Objects server, a single Rust binary whose only dependency is an object storage bucket. Write for Workers once and you keep an exit. The catch: the planned merge of celld into workerd is announced work, not shipped code, and you operate everything yourself.
Netlify Edge Functions
Netlify built its edge runtime on Deno and charges edge traffic as plain web requests, 2 credits for every 10,000. The example's 3M requests use 600 of the Personal plan's 1,000 monthly credits ($9), leaving 400 for bandwidth at 20 credits per GB and production deploys at 15 credits each. Each request gets 50 ms of CPU, which is tight for heavy AI post-processing.
Fly.io
Fly runs containers on small VMs, so you can keep Deno, Fresh and even KV: run Deno's self-hosted denokv server with its SQLite file on a volume and point Deno.openKv() at it. SaaSKit reads its KV path from an environment variable, so utils/db.ts needs no edits.
!Fly.io homepage
A shared-cpu-1x machine with 512 MB is $3.69 a month and volumes are $0.15 per GB. It buys time rather than a future, though, since Deno's own development ends after the final year.
Val Town
Val Town executes TypeScript snippets it calls vals on Deno 2.9.6; for webhooks, bots and small agents, nothing else here feels as close to Deploy. Each val gets a private SQLite database, and Pro is $25 a month ($21 billed yearly) with 1M runs a day and 10-minute runs. The free plan stops runs at 1 minute of wall clock and SQLite at 10 MB.
Vercel Functions
Vercel's official runtimes cover Node.js, Bun, Python, Rust, Go, Ruby, Wasm and Edge, while Deno only works through the community-maintained vercel-deno runtime, which means most teams will be porting code rather than moving it. Fluid compute bills CPU only while your code runs, though memory keeps billing during a wait. Pro is $20 a month with $20 of usage credit, and Active CPU is charged from minute one at $0.128 an hour in the iad1 (Washington, D.C.) region; the 4 free hours are a Hobby perk.
Choosing Without Migrating Twice
1. Count before you choose. An app with zero KV and cron hits can go anywhere.
2. Ask whether you need Deno to keep running. Fly.io runs it as is; Supabase, Netlify and Val Town run code written for it; Workers and Vercel mean porting.
3. Locate the KV code. When it lives in a single module, as in SaaSKit, moving it to Durable Objects or Postgres takes about a day.
4. Compare your slowest request with each CPU ceiling: 10 ms (Workers Free), 50 ms (Netlify), 2 s (Supabase).
5. Start early, since the migration help on offer only targets Workers.
If you are translating Deno.cron schedules, the Cron Expression Generator builds the expression for a Cron Trigger or pg_cron, and TypeScript to JavaScript shows what a handler compiles to before you port it to Node.js.
My Short List
- Safest bet for the next few years: Workers, with self-hosted workerd as a fallback.
- Already using Postgres or Supabase Auth: Supabase Edge Functions.
- Need it running this week: Fly.io plus denokv, with zero code edits.
- Bots, webhooks and small agents: Val Town.
- Heading to Next.js anyway: Vercel.
April 2027 is close, yet for most apps the real work sits in one storage module and maybe a scheduled job. Count the files that truly depend on Deno before committing to a host. The full DevToolLab write-up covers everything this version skips.
References
- 7 Best Deno Deploy Alternatives in 2026 (DevToolLab)
- Deno joins Cloudflare (Deno blog)
- Deno joins Cloudflare (Cloudflare blog)
- SaaSKit on GitHub
- workerd on GitHub
- celld on GitHub
- denokv on GitHub
- Netlify Edge Functions docs
- Vercel Functions pricing docs
5 reactions
Mentions
Deno's development is ending, so I moved my SaaS starter to Node and Bun Deno's team is joining Cloudflare, and the runtime's development ends after one more year of fixes. Seed, my SaaS starter, ran on Deno and Fresh, so I moved it to run on Deno, Node and Bun. This post is the honest account: what was hard, what wasn't, and the slowdown I shipped on the way. TL;DR - The Deno.* calls were not the hard part. Serving files, Deno types in public signatures and Fresh's internals were. - I replaced Fresh by copying its API: 419 of 423 responses came out identical, and every page shipped less JavaScript. - My first release of that layer made the slider post block input for 119 ms against Fresh's 51.5 ms. CI missed it: none of its pages has an island. - Don't rewrite. Put each Deno.* behind a small adapter, run the build on Node and Bun in CI, and diff every response before and after. Seed: Live: Open the live demo · Code: spy4x/seed To migrate from Deno to Node and Bun, you don't rewrite your app. You keep it on web standards (fetch, Request, Response), put every Deno.* call behind a small adapter, and replace what only runs on Deno, such as Fresh. I did exactly that for [Seed](/blog/seed-saas-starter), the SaaS starter every new project of mine starts from. The pull requests that finished it merged on 4 November 2026. This post is for a developer or a tech lead with a Deno codebase who read the news and now has to decide: stay, fork or move. Short version of the worry "my code is full of Deno.*": those calls were the easy part. It shows what was really tied to Deno in a tested codebase, what replaced each piece, and the mistake I shipped on the way. What happened to Deno? On 9 October 2026, Ryan Dahl wrote that Deno is joining Cloudflare. The runtime gets another year of monthly releases with bug fixes and security updates, and then: "After that year we will end our development of the Deno runtime." Deno stays open source, and others are welcome to continue it. The same post says Deno Deploy keeps running for six months and then shuts down, and JSR keeps running on Cloudflare's infrastructure. Cloudflare's own post says the team will merge Deno's celld into Cloudflare's workerd runtime. Neither post says what happens to Fresh, Deno's web framework. So nothing breaks tomorrow. Your app runs the same today as it did yesterday. What changes is the future: a bug or a security hole found after that year may never be fixed.  Why move a starter that worked? Seed is not one app. It is the starting point of every product I build, client MVPs included. A runtime that stops getting fixes under it would sit under all of them. So the goal was not "leave Deno". It was "run unchanged on Deno, Node and Bun". Deno stays both a runtime Seed supports and its toolchain: deno fmt, deno lint and deno test still run every check. If Deno finds a new home, nothing is lost. If it doesn't, the switch is a start file, not a rewrite. "But my code is full of Deno.*" That is the usual worry, and it was mine too. Here is the honest answer: the Deno.* calls were the easy part. Reading an environment variable, serving a request, opening a file: each has an obvious twin on Node, and Bun implements Node's node:fs and node:net (Bun's compatibility list). So one small adapter covers both. This is the start file that runs Seed's website on Node and Bun, trimmed of its port check. Deno runs the same built server with deno serve: import { serve, type ServeHandler } from "@spy4x/platform/server/serve" import { shutdownSignal } from "@spy4x/platform/server/shutdown-signal" import { exitProcess, processEnv } from "@spy4x/platform/server/process" const hostname = processEnv.get(`MPA_HOST`) || `0.0.0.0` const port = Number(processEnv.get(`MPA_PORT`) || 8000) const built: { default: { fetch: ServeHandler } } = await import( new URL(`./server.js`, import.meta.url).href ) const server = await serve(built.default.fetch, { hostname, port, signal: shutdownSignal(), }) await server.finished exitProcess(0) 1. processEnv reads Deno.env on Deno and process.env on Node and Bun. 2. The built server is a plain fetch handler: a Request in, a Response out. That is the web standard doing the heavy lifting. 3. serve() listens on whichever runtime it finds itself on. 4. shutdownSignal() is an AbortSignal that fires on SIGINT or SIGTERM, so requests in flight finish before the process exits. Even the Redis client needed nothing. It already talked over a connectTcp adapter in my shared library: Deno.connect on Deno, node:net on Node and Bun. I tested it on Valkey 8.1, Node 26.13 and Bun 1.4.4. So what was hard? Four things, and only one of them is a function call: 1. Default arguments that call Deno.*. A function with a default like fs = denoFs looks harmless until Node imports it, and there is no Deno to call. The defaults now pick the running runtime's version. 2. Deno types in exported signatures. Deno.Signal sat in public types, so a Node project could not even type-check my library. It became OsSignal, a list of the same 36 names, with a type test in both directions so the two can never drift apart. 3. Error classes. Deno throws Deno.errors.IsADirectory; Node throws an error with code: "EISDIR". One fsError and isFsError pair maps both. The Node adapter is imported lazily, so deno check doesn't pull in Node's types. 4. Serving files. Range requests and ETags took the most care. A test caught bytes=10- on a 10-byte file returning 200 instead of 416: a range that starts past the end of the file asks for nothing, and the right answer is "Range Not Satisfiable". A reviewer caught that a weak ETag must never satisfy If-Range, because the HTTP standard compares it with the strong comparison only. The final check was blunt: deno bundle of the 77 @spy4x/* imports Seed uses produced one .mjs file, and it ran directly on Node and Bun, with no package.json. All of that shipped as release 1.116.0 of my shared library. How do you replace Fresh without rewriting every route? Fresh was the real tie. It is Deno's own framework, built by the same company, and I had built Seed's public website on it. That is the mistake I own here: I put a framework owned by the runtime's company under the one project everything else starts from. I didn't rewrite the routes. I wrote a small layer that copies Fresh 2's API: App, ctx.render, createDefine, fsRoutes. It renders with Preact, builds with Vite and runs on web standards. It is @spy4x/preact-server, 1,459 lines of source and 919 lines of tests, part of my open-source component library. Because the API matched, Seed's route files barely changed. Seed's side of the switch, spy4x/seed#815, touched 128 files and deleted more than it added: +562, −908. Did anything change for the visitor? To find out, I requested every route on both builds and compared the answers (spy4x/seed#623). 419 of 423 responses were identical. Two of the four differences were one video's modification time and ETag. The other two were the malformed path /%E0%A4%A: Fresh answered with a bare 500, the new layer answers with the site's 404 page. The pages also got lighter. Gzipped JavaScript per page, Lighthouse mobile, 22 pages, median of 3 runs: | Page | New layer | Fresh | | ----------------------- | --------- | ------- | | Home | 24.0 KB | 32.1 KB | | A blog post with island | 29.9 KB | 38.0 KB | | Docs | 8.6 KB | 19.4 KB | Every page scored 98–100 on both builds. Of the 112 end-to-end tests, 110 passed on the first run. The two that failed tested Fresh's internals: an island's file name and the CSP nonce. What's better, and what's missing? Two things got better: - No nonce in the CSP. The layer puts island props in a JSON block, not an inline script, so the Content-Security-Policy needs no nonce. This very site still runs on Fresh, and its CSP code reads Fresh's private nonce. That is exactly the kind of internal I don't want to depend on. - Safer redirects. ctx.redirect() keeps /\host and /<tab>/host on the site (the test). Fresh lets both through to another host. And four things Fresh has that the layer doesn't: csrf() middleware (a same-origin check from my shared library does the job), _404 and _500 routes, its own dev server (Vite runs development instead), and a per-route opt-out of the modulepreload header. The scar: my replacement made the slider post slower than Fresh Seed's blog has a post with an interactive slider, the welcome post. On the first release of my layer, 13.11.0, that page blocked input for 119 ms. Fresh blocked it for 51.5 ms. That is Lighthouse 12.8.2, mobile, 6 alternating runs each. The page shipped less JavaScript and still blocked input more than twice as long. The cause: loading and hydrating every island ran as one long task. The browser could not answer a tap until the last island was awake.  The fix, in 13.11.1 (#1193), gives the browser a turn before each island: function yieldToMain(): Promise<void> { const scheduler = (globalThis as { scheduler?: { yield?: () => Promise<void> } }).scheduler return scheduler?.yield ? scheduler.yield() : new Promise((resolve) => setTimeout(resolve, 0)) } It uses scheduler.yield() where the browser has it and falls back to setTimeout(0). The blocking time dropped to 2 ms. The design reviewer re-checked it over 7 runs: 5 ms against Fresh's 48 ms, and a Lighthouse score of 97 against 96. Why didn't CI catch it? Seed's CI does run a Lighthouse speed check. But none of the pages it measures has an island (spy4x/seed#816). A speed check only guards the pages you give it. If your most expensive page isn't in the list, its regression ships green. > Measure the page with the heaviest island, not the page that's easiest to > measure. The trap: one line in package.json broke the tests The last pull request, spy4x/seed#817, added the Node and Bun start files, their Docker images and a CI smoke test that builds the site and runs it on all three runtimes. 27 files, +768, −212. Two surprises came with it. Adding "type": "module" to the root package.json broke Playwright under Deno. The fix was to leave the field out: that file only hands npm run and bun run over to deno task. And in CI, the new smoke test and the browser tests raced each other on vapid.json and the build folders. The fix: the smoke test now runs in its own copy of the checkout.  How did AI agents split the work? I didn't type most of this code. One coordinator session, an AI agent, started separate sessions for each lane: my shared library, the website layer in my component library, the Seed port, and landing the move off Fresh. Each worked in its own git worktree with its own pull request. An independent reviewer agent gated every merge. On changes a visitor can see, a design reviewer gated it too, in a real browser. They earned their place: the weak-ETag If-Range bug was a reviewer's find, and the 13.11.0 slowdown was the design reviewer's measurement. One honest lesson. When a session stopped halfway, the coordinator first only passed its report on to me instead of carrying the work on. I had to point that out. Now a coordinator continues a stopped session's work itself.  An agent also drafted this post. Every number in it comes from a merged pull request or from the measurements in spy4x/seed#623. How tied to Deno is your project? Here is every tie I found, and what replaced it in Seed. The first eleven were Seed's; the last three Seed never had: | Your project uses | What replaced it in Seed | | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Deno.serve or deno serve | The servers export a standard fetch handler. Deno still runs it with deno serve; on Node and Bun a start file hands it to serve() from @spy4x/platform. | | Deno.env | processEnv from @spy4x/platform: Deno.env on Deno, process.env on Node and Bun. | | File APIs: Deno.readFile, Deno.stat, Deno.open | A small file interface with two versions: Deno.* on Deno, node:fs/promises on Node and Bun. | | Deno's error classes, such as Deno.errors.NotFound | One fsError and isFsError pair that also knows Node's code: "EISDIR". | | Deno.Signal or other Deno types in exported signatures | OsSignal, a list of the same 36 names, with a type test in both directions. | | Default arguments that call Deno.* | Defaults that pick the running runtime's version. | | Deno.connect, such as a Redis client | Nothing: the client already ran over a connectTcp adapter, Deno.connect on Deno and node:net on Node and Bun. | | Fresh | @spy4x/preact-server, a new layer that copies Fresh 2's API. | | jsr: imports | deno bundle builds them into the server file, so Node and Bun never see a jsr: import. | | Deno.Command | Nothing: only the dev, deploy and media scripts use it, and they stay on Deno. | | deno test, deno fmt, deno lint | Nothing: the toolchain stays Deno. npm run and bun run call the same deno tasks. | | Deno KV | Never used: the data lives in Postgres and Valkey. | | Deno Deploy | Never used: Seed runs in Docker Compose on any Linux host. | | deno compile | Never used: Seed ships Docker images. | If you ticked Deno Deploy, start there: it has six months, not a year. If you ship single binaries with deno compile, as I do for [my MCP servers](/blog/building-mcp-servers-with-deno), that is a decision of its own. Tick your own project's ties below: The table above also works as a checklist you can tick for your own project. Try it on antonshubin.com: <antonshubin.com> Should you stay, fork or move? My answer: don't panic, and don't rewrite. You have a year of fixes. Use it to make the switch cheap, then decide when you have to: 1. Count your ties. The table above is a start. Look hardest at your public types and your default arguments: that is where Deno hides. 2. Put each Deno.* behind a small adapter. One function per need, with a Deno version and a Node version. Bun takes the Node one. 3. Run the build on Node and Bun in CI. A smoke test that starts the server on each runtime and requests a page catches the rest. 4. Replace the framework last, by copying its API. Then diff every response before and after, so "it works" is a number, not a feeling. 5. Measure your heaviest page. The one with the most islands, not the one that's easiest to measure. If your product runs on Deno and you'd like a calm plan instead of a panicked rewrite, that is what my [code and security audit](/catalog/codebase-health-audit) is for: three days, from $1,500. It covers architecture, security and performance, and for a Deno codebase that includes every tie to Deno, ranked by risk, with what to move first and what to leave alone. And if you've already started your own move, tell me what bit you. I'd like to know if it was the Deno.* calls or, like me, everything around them. --- _First published on antonshubin.com._
@deno.land team had have to do it since the beginning. If this step would be done, it might change the future of the project. And it seems like it would be done soon: deno#37030
@JeffKazzee Similar with Deno. Luckily the team is working on getting Deno into a foundation. deno#37030
写 TypeScript 小服务,还得先拼一堆工具?🦕 Deno 提供 JS/TS 运行时及内置开发工具。写脚本、做 Web API 都能试试。⚙️ denoland/deno #TypeScript #浇朋友 #蓝朋友 #互关 #fo
Bart, co-creator of Deno, says they're looking into a foundation now!! Source: deno#37030
Cloudflare bought Deno's team because of celld, yet there hasn't been commits for few weeks. Supposedly they wanted it because having 'standards based approach' is good for Cloudflare too, then why aren't they continuing it? @cloudflare.social @deno.land denoland/celld
Deno Deploy Is Shutting Down: Where to Move Your App, and How Big the Move Is The usual questions about where to run TypeScript functions are latency and cost. Deno Deploy users now have a blunter one: the platform is going away. On October 9, 2026, Ryan Dahl said the whole Deno team is moving to Cloudflare and that Deploy will shut down six months later, which puts the deadline around April 2027. I spent a day sizing the migration on a real codebase and pricing the main destinations. This is the condensed version; the complete guide on DevToolLab has the audit script, every pricing line and screenshots of each platform. What Ends and What Survives Per Deno's announcement, Deploy runs for six more months and then closes. Deno Land will keep shipping runtime patches, bug and security fixes once a month, for a year and then stop working on the runtime; the code stays open source, MIT licensed as it is today. JSR keeps operating while its infrastructure moves to Cloudflare, and rusty_v8 stays supported while the team plans to fold it into workerd. Paying customers get migration help, but the announcement only mentions moving to Cloudflare Workers. Deno Sandbox and Subhosting share Deploy's plans ($0 Free, $20 Pro, $200 Builder), and nobody has said what happens to them. Most of the Pain Lives in a Few Files Five kinds of Deno-only code make a Deploy app hard to move: KV reads and writes, KV queues, Deno.cron jobs, Deno.* file access and jsr: package imports. I wrote a small Node script that counts those patterns in a project, then ran it against SaaSKit, Deno's own Fresh SaaS starter built for Deploy (now archived, last commit February 9, 2026): 69 source files scanned Deno.openKv() 1 hits in 1 files KV atomic / watch / listenQueue 5 hits in 1 files Deno.cron 0 hits in 0 files BroadcastChannel 0 hits in 0 files Deno.serve 0 hits in 0 files Deno.env 5 hits in 3 files Deno filesystem calls 5 hits in 2 files jsr: imports 10 hits in 2 files npm: imports 8 hits in 2 files https:// URL imports 1 hits in 1 files The interesting part is concentration. All of SaaSKit's KV usage sits in utils/db.ts, a 483-line module with 15 KV calls that 22 other files import. Swapping storage means rewriting that one module, not the app. The script itself is about 40 lines and is in the original post if you want to point it at your own repo. Seven Places to Go The worked example throughout: a SaaSKit-shaped app with an AI chat endpoint, KV for users and sessions, and roughly 3 million requests a month. | Platform | Deno code unchanged? | KV replacement | Example month | |---|---|---|---| | Cloudflare Workers | No | Workers KV, D1, Durable Objects | $5 | | Supabase Edge Functions | Mostly | Postgres | $27 | | workerd + celld | No | Durable Objects | your servers | | Netlify Edge Functions | Mostly | Netlify Blobs | $9 | | Fly.io | Yes, it runs Deno | self-hosted denokv | about $4 | | Val Town | Mostly | per-val SQLite | $25 | | Vercel Functions | Community runtime only | Blob or a database | $20 and up | Cloudflare Workers The Deno engineers now work on this platform, and it is the one target the migration offer names. Durable Objects give each object its own SQLite database plus WebSockets, a good fit for agent-style workloads, and Deno.cron maps to Cron Triggers. !Cloudflare Workers homepage There is no Deno global here, so SaaSKit's database module needs rewriting, as does each Deno.env lookup. The Paid plan is $5 a month with 10M requests and 30M CPU milliseconds included; at 5 ms of CPU per request and KV inside its included reads and writes, the example stays at $5. The free plan caps CPU at 10 ms per invocation. Supabase Edge Functions Supabase's Edge Runtime is Deno-compatible and MIT licensed, so most handler code moves with light edits, and Postgres replaces KV with pg_cron for schedules. Each hosted invocation is limited to 2 seconds of CPU time and 256 MB of RAM. Pro is $25 a month with 2M invocations, then $2 per million, so the example lands at $27. workerd and celld, Self-Hosted workerd is the Apache 2.0 runtime under Workers, and celld is Deno's self-hosted Durable Objects server, a single Rust binary whose only dependency is an object storage bucket. Write for Workers once and you keep an exit. The catch: the planned merge of celld into workerd is announced work, not shipped code, and you operate everything yourself. Netlify Edge Functions Netlify built its edge runtime on Deno and charges edge traffic as plain web requests, 2 credits for every 10,000. The example's 3M requests use 600 of the Personal plan's 1,000 monthly credits ($9), leaving 400 for bandwidth at 20 credits per GB and production deploys at 15 credits each. Each request gets 50 ms of CPU, which is tight for heavy AI post-processing. Fly.io Fly runs containers on small VMs, so you can keep Deno, Fresh and even KV: run Deno's self-hosted denokv server with its SQLite file on a volume and point Deno.openKv() at it. SaaSKit reads its KV path from an environment variable, so utils/db.ts needs no edits. !Fly.io homepage A shared-cpu-1x machine with 512 MB is $3.69 a month and volumes are $0.15 per GB. It buys time rather than a future, though, since Deno's own development ends after the final year. Val Town Val Town executes TypeScript snippets it calls vals on Deno 2.9.6; for webhooks, bots and small agents, nothing else here feels as close to Deploy. Each val gets a private SQLite database, and Pro is $25 a month ($21 billed yearly) with 1M runs a day and 10-minute runs. The free plan stops runs at 1 minute of wall clock and SQLite at 10 MB. Vercel Functions Vercel's official runtimes cover Node.js, Bun, Python, Rust, Go, Ruby, Wasm and Edge, while Deno only works through the community-maintained vercel-deno runtime, which means most teams will be porting code rather than moving it. Fluid compute bills CPU only while your code runs, though memory keeps billing during a wait. Pro is $20 a month with $20 of usage credit, and Active CPU is charged from minute one at $0.128 an hour in the iad1 (Washington, D.C.) region; the 4 free hours are a Hobby perk. Choosing Without Migrating Twice 1. Count before you choose. An app with zero KV and cron hits can go anywhere. 2. Ask whether you need Deno to keep running. Fly.io runs it as is; Supabase, Netlify and Val Town run code written for it; Workers and Vercel mean porting. 3. Locate the KV code. When it lives in a single module, as in SaaSKit, moving it to Durable Objects or Postgres takes about a day. 4. Compare your slowest request with each CPU ceiling: 10 ms (Workers Free), 50 ms (Netlify), 2 s (Supabase). 5. Start early, since the migration help on offer only targets Workers. If you are translating Deno.cron schedules, the Cron Expression Generator builds the expression for a Cron Trigger or pg_cron, and TypeScript to JavaScript shows what a handler compiles to before you port it to Node.js. My Short List - Safest bet for the next few years: Workers, with self-hosted workerd as a fallback. - Already using Postgres or Supabase Auth: Supabase Edge Functions. - Need it running this week: Fly.io plus denokv, with zero code edits. - Bots, webhooks and small agents: Val Town. - Heading to Next.js anyway: Vercel. April 2027 is close, yet for most apps the real work sits in one storage module and maybe a scheduled job. Count the files that truly depend on Deno before committing to a host. The full DevToolLab write-up covers everything this version skips. References - 7 Best Deno Deploy Alternatives in 2026 (DevToolLab) - Deno joins Cloudflare (Deno blog) - Deno joins Cloudflare (Cloudflare blog) - SaaSKit on GitHub - workerd on GitHub - celld on GitHub - denokv on GitHub - Netlify Edge Functions docs - Vercel Functions pricing docs
There's a fork-in-progress called "Dodo," the Discord server is quickly filling up. deno#37030 Dodo aims to be a drop-in replacement. Then there's Ende, my fork, and I ripped out Node compatibility, Deploy, KV, Jupyter, &c. ende.sh
Deno GitHub Explained: Secure-by-Default JavaScript, Native TypeScript, and Permission Boundaries deno by denoland: denoland/deno Deno is a JavaScript and TypeScript runtime built around V8, Rust, and Tokio. This overview explains how its secure-by-default permission model changes the way network, disk, and environment access are granted, while a single statically linked binary consolidates the runtime and its developer tooling. The architecture section connects the runtime stack to native TypeScript execution and standard web Request/Response APIs. It then examines the permission boundary around a minimal HTTP server, including explicit --allow-net access, least-privilege scoping, and --no-prompt behavior for CI pipelines. The final section covers standalone compilation, static import analysis, and why dynamically interpolated imports need explicit inclusion or refactoring. TimeStamps: 0:00 Implicit Trust and Supply Chain Attacks 0:39 V8, Rust, and Tokio 1:14 Third-Party Configuration Files 1:40 Static Binary Distribution 2:24 Native TypeScript HTTP Server 3:11 Restricted Sandbox and Network Access 3:48 Allow-Net Permission Scope 4:16 No-Prompt in CI/CD 4:50 Compile to a Standalone Executable 5:12 Static Import Graph Limits 5:54 Security and Developer Ergonomics 🛡️ Secure-by-default permissions and least-privilege network access 🧩 V8, Rust, Tokio, native TypeScript, and unified tooling 📦 Standalone compilation, static imports, and dynamic module limits Deno's tradeoff is clear: fewer external layers and stronger runtime boundaries, with explicit permissions and statically analyzable dependencies as the operating model. It suits teams that value native TypeScript and auditable CI behavior, while compiled applications must account for dynamic imports. Use the repository overview to evaluate where that constraint fits your architecture. #Deno #TypeScript #JavaScriptRuntime [de-DE] YNAS ynp_054adb49f87b63c6 Kinofilm Privater automatisierter Staging-Upload. Paketidentität: ynp_054adb49f87b63c6; Ausgabe: cinematic. [es-US] YNAS ynp_054adb49f87b63c6 cinemático Carga de entorno de prueba de automatización privada. Identidad del paquete: ynp_054adb49f87b63c6; salida: cinemática. [fr-FR] YNAS ynp_054adb49f87b63c6 cinématique Téléchargement de l'environnement de test d'automatisation privé. Identifiant du package : ynp_054adb49f87b63c6 ; sortie : cinématique. [hi] YNAS ynp_054adb49f87b63c6 सिनेमैटिक निजी स्वचालन स्टेजिंग अपलोड। पैकेज पहचान: ynp_054adb49f87b63c6; आउटपुट: सिनेमाई। [id] YNAS ynp_054adb49f87b63c6 sinematik Unggahan staging otomatisasi pribadi. Identitas paket: ynp_054adb49f87b63c6; output: sinematik. [it] YNAS ynp_054adb49f87b63c6 cinematic Caricamento di automazione privata. Identità del pacchetto: ynp_054adb49f87b63c6; output: cinematografico. [pt-BR] YNAS ynp_054adb49f87b63c6 cinemático Upload de teste de automação privada. Identidade do pacote: ynp_054adb49f87b63c6; saída: cinemática.
The state of Deno after the Cloudflare announcement deno#37030