Skip to main content

How a Bunway request works

A generated Bunway application is two cooperating applications during development:

Browser
├── UI requests ───────────────→ SvelteKit/Vite :5173
└── API, SSE, and WebSocket ───→ Elysia/Bun :3000

├── validation: Elysia `t`
├── queries: Drizzle
├── connection: Bun.SQL
└── data: PostgreSQL

Bunway generates and connects the files, but it does not insert a controller, repository, serializer, or proprietary rendering layer into this path.

The complete mental map

Developer starts `bunway dev`

├── Bun watches `src/app.ts`
│ └── Elysia composes middleware and explicitly registered route modules

└── Bun starts Vite in `web/`
└── SvelteKit serves the browser application and recompiles changed Svelte files

Browser opens /products

├── SvelteKit selects web/src/routes/products/+page.svelte
├── the root +layout.svelte renders the sidebar, header, and page
├── the page creates an Eden Treaty client typed from `App`
└── the page requests GET http://localhost:3000/products

├── CORS middleware accepts the configured browser origin
├── Elysia matches the generated Products route
├── Elysia validates query/path/body input when applicable
├── the route executes an explicit Drizzle query
├── Drizzle sends parameterized SQL through Bun.SQL
├── PostgreSQL executes the query and returns rows
├── Drizzle maps rows to typed JavaScript objects
└── Elysia serializes the route result as an HTTP response

├── Eden exposes the inferred result/error type
├── the Svelte page assigns the result to reactive state
└── Svelte updates only the affected DOM

That tree is the central Bunway model: generated source coordinates direct library APIs.

1. Application startup

bunway dev starts two child processes under Bun:

bun --watch src/app.ts
bun run --bun --cwd web dev

The backend entry point is src/app.ts:

export const app = new Elysia()
.use(cors(...))
.use(realtimeRoutes)
.use(routes)

export type App = typeof app

.use(routes) imports the explicit composition from src/routes/index.ts. A generator adds imports and .use(...) calls at marker comments. Bunway does not scan the filesystem at runtime.

export type App = typeof app preserves the entire Elysia contract. The frontend imports this as a type, so Eden Treaty knows the route paths, input shapes, and response shapes without a separate DTO or generated client file.

2. The initial page request

When the browser opens http://localhost:5173/products, SvelteKit maps the URL to:

web/src/routes/+layout.svelte persistent application shell
web/src/routes/products/+page.svelte collection page

The layout renders the sidebar and then {@render children()} renders the selected page. The generated preview currently loads CRUD data in the browser, so Vite/SvelteKit first returns the application HTML and JavaScript; the mounted page then calls the API. Applications remain free to move data loading into SvelteKit server load functions when SSR is required—Bunway does not wrap that mechanism.

3. Eden creates the typed request

Generated pages construct the normal Eden Treaty client:

import { treaty } from '@elysiajs/eden'
import type { App } from '../../../../src/app'

const api = treaty<App>('http://localhost:3000')
const result = await api.products.get({ query: { page: 1 } })

The App type is erased at runtime. The browser sends an ordinary HTTP request. Eden's value is compile- time inference: if a route path, body, or response changes, TypeScript can identify frontend calls that must change with it.

4. Elysia receives and validates it

The request reaches Bun's HTTP server and flows through the Elysia composition order in src/app.ts. For a generated resource, Elysia then matches a route in src/routes/products.ts.

new Elysia({ prefix: '/products' }).post(
'/',
async ({ body, status }) => {
const [product] = await db.insert(products).values(body).returning()
return status(201, product)
},
{ body: productInput },
)

The schema passed in { body: productInput } validates untrusted input before the handler uses it. An invalid body receives a validation response; it does not reach the insert. The handler is still plain Elysia code and may be edited directly.

5. Drizzle and Bun.SQL reach PostgreSQL

src/db/index.ts owns the runtime connection:

export const db = postgresDrizzle(new Bun.SQL(required('DATABASE_URL', Bun.env.DATABASE_URL)))

The generated handler imports db and a table from src/db/schema. Drizzle translates its typed query builder into parameterized SQL, and Bun.SQL sends it to PostgreSQL. Bunway does not introduce a model base class or repository between them.

Drizzle Kit is separate tooling. bunway db:migrate reads drizzle.config.ts, compares the Drizzle schema to migration history, writes SQL under src/db/migrations, and applies it. Runtime requests do not invoke Drizzle Kit.

6. The response becomes rendered UI

The handler returns an ordinary value. Elysia serializes it, Eden returns { data, error }, and the Svelte page updates reactive state:

const result = await api.products.get()
if (result.error) message = 'Could not load products'
else products = result.data.items

Svelte tracks reads of products in the template and updates the table when that state changes. There is no Bunway renderer. Tailwind classes style the markup, while the checked-in shadcn-svelte components provide editable UI primitives.

Create, update, and delete requests

Mutations follow the same path with different HTTP methods:

Svelte form
→ Eden POST/PATCH/DELETE
→ Elysia validation
→ generated route handler
→ Drizzle insert/update/delete
→ Bun.SQL
→ PostgreSQL transaction
→ typed response
→ Svelte state refresh

Expected failures remain visible as error results. Generated destructive actions request confirmation in the UI, but authorization and business rules belong in the application route because browser checks are not a security boundary.

Attachments

Attachment fields deliberately do not add binary columns to a resource table:

browser multipart upload
→ generated resource attachment endpoint
→ Storage adapter writes bytes (local disk or Bun S3)
→ Drizzle inserts storage_blobs metadata
→ Drizzle inserts storage_attachments polymorphic link
→ response contains attachment metadata/URL
→ Svelte renders preview or count

The resource row and attachment records remain independently understandable. Storage adapters manage bytes; PostgreSQL remains the metadata source of truth.

Jobs

performNow(payload) calls the Job handler in the current process. performLater(payload) inserts a row into PostgreSQL:

request handler calls performLater
→ Jobs table receives payload, queue, and schedule
→ HTTP request can finish
→ `bunway worker` claims with PostgreSQL locking
→ worker imports src/jobs/index.ts
→ registered Job handler executes
→ row records completion, failure, or retry

The explicit src/jobs/index.ts registry ensures workers know which application Job names they can execute. Redis and a separate queue service are not required.

Realtime

Both transports use Bun/Elysia endpoints already mounted by realtimeRoutes:

SSE: browser subscribes → API keeps HTTP stream open → channel.publish sends server events
WebSocket: browser connects → either side sends typed events → channel broadcasts to peers

The current preview broker is process-local. A Job executed by a separate worker persists correctly but cannot push through the API process's in-memory broker. Use same-process execution for a visible progress demo, or add an application-appropriate cross-process transport when the requirement is real.

Where to debug each failure

SymptomFirst place to inspect
Page URL is missingweb/src/routes, then web/src/lib/resources.ts for navigation
API returns 404src/routes/index.ts explicit import and .use(...)
API returns validation errorElysia input schema in the resource route
Database connection fails.env, src/db/index.ts, and PostgreSQL availability
Schema and database differdrizzle.config.ts, schema exports, then bunway db:migrate
Frontend type no longer matchesexported App type and the Eden call site
UI does not refreshSvelte state assignment after the request
Job remains queuedworker process, src/jobs/index.ts, queue selection, and Jobs table
Realtime connects but receives nothingchannel name/parameters and process boundary

Next, see the concrete project structure that owns every part of this flow.