Your Express app, rewritten in sym — every concept side by side, the real API, and an honest list of what doesn't work yet.
You keep writing TypeScript with the same shape you already know — a router, route handlers, req/res-style objects — but instead of node server.js you run sym server.ts, and every handler is compiled to CUDA and executed on the GPU instead of interpreted on a CPU.
No node_modules to ship. No V8. The @symmetra/* packages are type definitions only — the implementations live inside the sym binary.
In Node.js, your JavaScript is interpreted by V8 on a CPU thread, one event loop at a time. In Symmetra, your TypeScript is compiled ahead of the first request into GPU machine code, and each request runs as GPU threads. The code you write looks almost identical. What changes is underneath.
| Node.js | Symmetra | |
|---|---|---|
| Language | JavaScript / TypeScript | The same TypeScript |
| Runs on | V8 → CPU, one event loop | CUDA → GPU, thousands of threads |
| How code executes | JIT-interpreted per request | Compiled to PTX once at startup, then GPU-native |
| Start command | node server.js | sym server.ts |
| Dependencies | npm install, ship node_modules/ | Types only; no runtime deps to ship |
| CPU does | Everything | Only TCP accept / recv / send |
Here is the smallest possible server in each. Read them side by side — the shape is the same: make an app, define a route, start listening.
const express = require('express')
const app = express()
app.get('/ping', (req, res) => {
res.json({ message: 'pong' })
})
app.listen(8080)
import { Router, serve } from '@symmetra/http'
const app = new Router()
app.get('/ping', ctx => {
ctx.json({ message: 'pong' })
})
serve(app, { port: 8080 })
Three differences, and that's the whole story of hello-world:
require('express') → import { Router, serve } from '@symmetra/http'ctx instead of (req, res) — it's one object that carries both.app.listen(port) → serve(app, { port })$ node server.js
# (silent — it's just listening)
$ curl localhost:8080/ping
{"message":"pong"}
$ sym server.ts
Listening http://localhost:8080
Routes 1 compiled
GPU grid 256 slots · persistent async
GET /gpu-stats for live metrics
$ curl localhost:8080/ping
{"message":"pong"}
On boot, sym prints a hexagon banner, your GPU model, VRAM, and the compile pipeline. The response your client gets is byte-for-byte what Express would send.
Most of migrating is muscle memory. Here's the direct mapping for the things you reach for every day.
| What you want | Node / Express | Symmetra |
|---|---|---|
| Create the app | express() | new Router() |
| A GET route | app.get('/x', (req,res)=>…) | app.get('/x', ctx =>…) |
| A POST route | app.post('/x', …) | app.post('/x', …) |
| Send JSON | res.json(obj) | ctx.json(obj) |
| Set a status code | res.status(404).json(…) | ctx.status(404).json(…) |
| Read a route param | req.params.id | ctx.params.id |
| Read the JSON body | req.body (after body-parser) | await ctx.json<T>() |
| Start the server | app.listen(8080) | serve(app, { port: 8080 }) |
| Environment variable | process.env.FOO | process.env.FOO |
| Run it | node server.js | sym server.ts |
You don't have to rewrite your imports. The compiler recognises common Node built-ins and points them at the GPU-native equivalent automatically:
| Your Node import | Becomes |
|---|---|
import fs from 'fs' | @symmetra/fs |
import path from 'path' | built-in path utilities |
import { createHash } from 'crypto' | @symmetra/crypto |
import { EventEmitter } from 'events' | @symmetra/core event emitter |
import { fetch } from 'node-fetch' | @symmetra/net |
import os from 'os' | GPU system-info API |
A CRUD-style handler with a URL param, a JSON body, and a 404 path. This is the shape 90% of API code takes — notice how little actually changes.
app.get('/user/:id', (req, res) => {
const id = req.params.id
const user = lookup(id)
if (!user) {
return res.status(404)
.json({ error: 'not found' })
}
res.json(user)
})
app.post('/user', (req, res) => {
const body = req.body
save(body)
res.json({ ok: true })
})
app.get('/user/:id', ctx => {
const id = ctx.params.id
const user = lookup(id)
if (!user) {
return ctx.status(404)
.json({ error: 'not found' })
}
ctx.json(user)
})
app.post('/user', async ctx => {
const body = await ctx.json()
save(body)
ctx.json({ ok: true })
})
await ctx.json() instead of req.body. There's no separate body-parser middleware to install — parsing is built in.
The everyday service libraries have direct equivalents. They import as normal TypeScript; the implementation runs inside sym, so there's nothing to npm install at runtime.
import { Router, serve } from '@symmetra/http'
import { KvStore } from '@symmetra/kv'
const kv = new KvStore({ maxMb: 512 }) // lives in GPU memory
const app = new Router()
app.get('/item/:id', async ctx => {
const item = await kv.get(ctx.params.id) // no DRAM round-trip
if (!item) { ctx.status(404).json({ error: 'not found' }); return }
ctx.json(item)
})
app.put('/item/:id', async ctx => {
const body = await ctx.json()
await kv.set(ctx.params.id, body, { ttlMs: 60_000 })
ctx.json({ ok: true })
})
serve(app, { port: 8080 })
In Node this would be ioredis + a Redis server. Here the cache is a slab of VRAM the GPU reads directly.
import { postgres } from '@symmetra/db'
import { jwt, password } from '@symmetra/crypto'
import { http } from '@symmetra/net'
const pg = postgres({ host: 'localhost', database: 'myapp', user: 'u', password: 'p' })
app.get('/users', async ctx => {
const r = await pg.query('SELECT id, name FROM users')
ctx.json(r.rows)
})
app.post('/login', async ctx => {
const { email, pass } = await ctx.json()
if (!await password.verify(pass, user.passwordHash)) {
ctx.status(401).json({ error: 'invalid credentials' }); return
}
const token = await jwt.sign({ sub: String(user.id) }, process.env.JWT_SECRET!, { expiresIn: 86400 })
ctx.json({ token })
})
app.get('/proxy', async ctx => {
const res = await http.get('https://api.example.com/data', { timeout: 5000 })
ctx.json(res.json())
})
ctx.fetch(…), kv, or pg, the GPU hands that one operation back to the CPU, the CPU performs it, and the result is handed back to the GPU — a cooperative round-trip that keeps your handler code looking like plain async/await.
This is the reason to be here. Array operations on a GpuBuffer — .map(), .filter(), .reduce() — compile to real CUDA kernels, one GPU thread per element. The TypeScript reads like an ordinary array method; the machine code is massively parallel.
app.post('/transform', async ctx => {
// already in VRAM, zero-copy
const data = await ctx.body<Float32Array>()
// one thread per element
const out = data.map(x => x*x + Math.sin(x))
ctx.json(out)
})
__global__ void transform(
const float* in,
float* out, int n) {
int tid = blockIdx.x*blockDim.x
+ threadIdx.x;
if (tid >= n) return;
float x = in[tid];
out[tid] = x*x + sinf(x);
}
In Node, that .map() is a sequential loop on one core. In Symmetra it's thousands of elements computed at once. You didn't write CUDA — you wrote .map().
Symmetra is public beta. It runs on a GPU, and GPUs are not CPUs — some things you take for granted in Node either don't exist or have sharp edges. Read this before porting anything real.
| Feature | Status | What to do |
|---|---|---|
| Express middleware ecosystem | ✗ No | No app.use(...) plugin chain. Write logic directly in handlers. |
| npm packages at runtime | ✗ No | Only the built-in @symmetra/* libraries run. Arbitrary npm modules don't. |
| Long-running loops with dynamic bounds | △ Careful | The compiler prefers loops with fixed, known bounds. See the coverage matrix. |
| Full JS/TS language surface | △ Subset | A large subset compiles; some dynamic patterns don't. Check the matrix below. |
| WebSockets / streaming responses | △ Limited | The model is request/response today. Confirm your protocol is supported. |
| Runs without an NVIDIA GPU | ✗ No | Needs an NVIDIA GPU (compute capability 7.5+) and a recent driver. |
There are no default routes — not even /health. You define your own routes (including any health check you want). But the runtime always exposes one endpoint for live diagnostics, no matter what your server.ts contains:
$ curl localhost:8080/gpu-stats
{
"gpu": { "name": "NVIDIA GeForce RTX 5070", "sm_arch": "sm_120",
"cuda_cores": 6144, "vram_total_gb": 12.82 },
"deployed": { "blocks": 32, "threads_total": 4096, "slots_total": 256 },
"usage": { "slots_in_flight": 0, "slots_free": 256 },
"live": { "power_w": 37.4, "temp_c": 34 },
"throughput": { "uptime_sec": 134.6, "total_requests": 0, "avg_rps": 0 }
}
It reports GPU topology, how much of the device you've deployed, live power/temperature, and cumulative throughput — the equivalent of a Prometheus exporter, built in and always reachable.
In Node you'd reach for the cluster module or PM2 to use more than one core. In Symmetra, the runtime sizes itself to your GPU at startup — it detects the number of streaming multiprocessors, your OS, and lays out GPU blocks, threads, and request slots automatically. A bigger GPU is used more fully with no code change.
sym server.ts auto-scales to the hardware it finds.--gpu-blocks, --gpu-threads, and matching SYM_* environment variables pin any knob for containers and orchestration..zip, or Linux .tar.gz) and run sym doctor to confirm your GPU and driver.server.ts, change (req,res) to ctx, res.json to ctx.json, and app.listen to serve(app, …).sym server.ts, then curl your route. The boot banner tells you how many routes compiled.@symmetra/* Replace ioredis → @symmetra/kv, pg → @symmetra/db, etc. Node built-ins remap automatically./gpu-stats and define whatever custom health route your infra expects.