Vision
Vision Server

Vision Server Overview

Meta-framework with built-in observability

Vision Server

Vision Server is a Bun-first, Elysia-based meta-framework with automatic observability, pub/sub, and end-to-end type safety (Eden-ready).

Vision Server wraps Elysia — the underlying instance is still an Elysia app, so every Elysia plugin (@elysia/cors, @elysia/jwt, @elysiajs/swagger, …) works, and the type chain flows into the Eden Treaty client.

Why Vision Server?

  • vs NestJS — orders of magnitude faster, zero DI ceremony, built-in observability
  • vs Encore.ts — 100% open source, no vendor lock-in
  • vs plain Elysia — adds tracing, logs, API Explorer, pub/sub, cron, rate limiting
  • Type-safe end-to-endtreaty<AppType> on the client, response shapes inferred from server schemas

Quick Start

bun add @getvision/server elysia zod
import { createVision, createModule, defineEvents } from '@getvision/server'
import { z } from 'zod'

const usersModule = createModule({ prefix: '/users' })
  .use(
    defineEvents({
      'user/created': {
        schema: z.object({ userId: z.string(), email: z.string().email() }),
        handler: async (event) => {
          console.log('[welcome email] →', event.email)
        },
      },
    })
  )
  .get('/', ({ span }) => {
    const users = span('db.select', { 'db.table': 'users' }, () => [
      { id: '1', name: 'Alice' },
    ])
    return { users }
  })
  .post(
    '/',
    async ({ body, emit }) => {
      const id = crypto.randomUUID()
      await emit('user/created', { userId: id, email: body.email })
      return { id, ...body }
    },
    {
      body: z.object({ name: z.string(), email: z.string().email() }),
    }
  )

const app = createVision({
  service: { name: 'My API', version: '1.0.0' },
  pubsub: { devMode: true },
}).use(usersModule)

app.listen(3000)

/** Eden Treaty client type — export for the frontend. */
export type AppType = typeof app

That's it! You get:

  • ✅ Vision Dashboard on port 9500
  • ✅ Automatic request tracing
  • ✅ Type-safe validation (Zod / Valibot / TypeBox via Standard Schema)
  • ✅ Service & event catalog
  • ✅ Eden Treaty type export for your frontend

The Module Pattern

Vision composes apps from small, focused modules. A module is an Elysia plugin with Vision's per-request context (span, emit, addContext, traceId) already typed in.

const ordersModule = createModule({ prefix: '/orders' })
  .use(defineEvents({ 'order/placed': { schema, handler } }))
  .post('/', async ({ body, emit }) => {
    await emit('order/placed', { orderId: '...', total: body.total })
    return { ok: true }
  }, { body: OrderBody })

Compose at the root via .use() — order doesn't matter and each module contributes its own routes, events, and crons:

const app = createVision({ service: { name: 'Shop' } })
  .use(usersModule)
  .use(ordersModule)
  .use(productsModule)

Learn more about modules →

Core Features

🚀 Zero configuration

Everything works out of the box:

  • Vision Dashboard starts automatically on port 9500
  • Tracing hooks are pre-wired (onRequest, onAfterResponse, onError)
  • Service & route catalog auto-registered (once on listen, or lazily on first request for .handle(req) deployments like Next.js)

🔥 span / emit / addContext in the handler

Every handler receives Vision's context alongside Elysia's body/query/params:

.post('/', async ({ body, span, emit, addContext }) => {
  addContext({ 'user.email': body.email })

  const user = span('db.insert', { 'db.table': 'users' }, () => {
    return db.users.create(body)
  })

  await emit('user/created', { userId: user.id, email: user.email })
  return user
})

✅ Validation with any Standard Schema library

Use Zod, Valibot, or Elysia's built-in TypeBox (t) — all interchangeable:

import { t } from '@getvision/server'
import { z } from 'zod'
import * as v from 'valibot'

// Zod
.post('/', handler, { body: z.object({ name: z.string() }) })

// Valibot
.post('/', handler, { body: v.object({ name: v.string() }) })

// Elysia / TypeBox — re-exported from @getvision/server
.post('/', handler, { body: t.Object({ name: t.String() }) })

Invalid request? Elysia returns a 422 with validation details automatically.

📊 Built-in pub/sub & cron

Powered by BullMQ (in-memory during development, Redis in production):

const notificationsModule = createModule()
  .use(
    defineEvents({
      'user/created': {
        schema: z.object({ userId: z.string(), email: z.string() }),
        handler: async (event) => {
          await sendWelcomeEmail(event.email)
        },
      },
    })
  )

Schedule work with cron:

import { defineCrons } from '@getvision/server'

const cleanupModule = createModule().use(
  defineCrons({
    'nightly-cleanup': {
      schedule: '0 0 * * *',
      handler: async () => {
        console.log('running cleanup')
      },
    },
  })
)

🎯 Elysia-native

Vision's underlying instance is Elysia — every plugin and feature works unmodified:

import { cors } from '@elysia/cors'
import { swagger } from '@elysiajs/swagger'

const app = createVision({ service: { name: 'My API' } })
  .use(cors())
  .use(swagger())
  .use(usersModule)
  .get('/health', () => ({ status: 'ok' }))

🔒 Per-endpoint rate limiting

import { rateLimit } from '@getvision/server'

.post('/', handler, {
  body: SignupBody,
  beforeHandle: [rateLimit({ requests: 5, window: '15m' })],
})

Learn more →

🧩 Eden Treaty — typed RPC on the client

import { treaty } from '@elysia/eden'
import type { AppType } from './server'

const api = treaty<AppType>('http://localhost:3000')

const { data } = await api.users.get()
//         ^? { users: { id: string; name: string }[] }

Change a schema on the server → the client gets a compile error. No codegen.

Configuration

const app = createVision({
  service: {
    name: 'My API',
    version: '1.0.0',
    description: 'Optional description',
    integrations: {
      database: 'postgresql://localhost/mydb',
      redis: 'redis://localhost:6379',
    },
    drizzle: {
      autoStart: true, // auto-start Drizzle Studio in dev
      port: 4983,
    },
  },
  vision: {
    enabled: true,    // enable/disable dashboard
    port: 9500,       // dashboard port
    maxTraces: 1000,
    maxLogs: 10000,
    logging: true,
    // exporters: [...] // forward traces to OTLP backends — see below
  },
  pubsub: {
    devMode: true,    // in-memory queue — no Redis required
    // redis: { host: 'localhost', port: 6379 }
  },
})

Exporting Traces (OTLP)

Forward every completed trace to any OpenTelemetry-compatible backend — BetterStack, Honeycomb, Grafana Tempo, Datadog, an OTel Collector — by adding an OtlpTraceExporter to vision.exporters. Export runs alongside the local Dashboard, so traces appear in both.

import { createVision, OtlpTraceExporter } from '@getvision/server'

createVision({
  service: { name: 'my-api' },
  vision: {
    exporters: [
      new OtlpTraceExporter({
        endpoint: 'https://<host>/v1/traces',         // OTLP/HTTP traces endpoint
        headers: { Authorization: 'Bearer <token>' }, // backend auth
        serviceName: 'my-api',
      }),
    ],
  },
})

The exporter speaks OTLP/JSON over HTTP, so the destination is purely a matter of endpoint + headers — switching backends is a config change, not a code change. Traces are buffered and flushed in batches, and a failing exporter is isolated so it never affects request handling.

Each HTTP request becomes a synthetic root span (SERVER) with your c.span(...) calls as nested INTERNAL spans, and the trace's logs attached as span events.

For a custom sink, implement the TraceExporter interface (export(trace) plus an optional shutdown()) and add it to vision.exportersOtlpTraceExporter is just the built-in implementation.

Next Steps