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: { traces: [...], logs: [...] } // forward to OTLP backends — see below
  },
  pubsub: {
    devMode: true,    // in-memory queue — no Redis required
    // redis: { host: 'localhost', port: 6379 }
  },
})

OTLP Export

Forward observability data to any OpenTelemetry-compatible backend — BetterStack, Honeycomb, Grafana Tempo, Datadog, an OTel Collector — via the exporters config. Traces and logs are configured independently under the same exporters object. Export runs alongside the local Dashboard.

Trace Export

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

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

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

Log Export

All console.* calls captured by Vision can be forwarded as OTLP LogRecords. Logs inside a trace carry the traceId for backend correlation.

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

createVision({
  service: { name: 'my-api' },
  vision: {
    exporters: {
      logs: [
        new OtlpLogExporter({
          endpoint: 'https://<host>/v1/logs',
          headers: { Authorization: 'Bearer <token>' },
          serviceName: 'my-api',
        }),
      ],
    },
  },
})

Traces and logs work side by side:

exporters: {
  traces: [new OtlpTraceExporter({ endpoint: '.../v1/traces', ... })],
  logs:   [new OtlpLogExporter({ endpoint: '.../v1/logs', ... })],
}

Both exporters share the same options and behaviour: OTLP/JSON over HTTP, buffered batch flushing, automatic retry on failure, and full error isolation.

For custom sinks, implement TraceExporter or LogExporter and add them to the respective array.

Next Steps

On this page