raiko.js 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.es.md +508 -0
  3. package/README.md +507 -2
  4. package/README.pt-BR.md +508 -0
  5. package/bin/raiko.mjs +149 -0
  6. package/dist/adapters/node-ws.d.ts +40 -0
  7. package/dist/adapters/node.d.ts +6 -0
  8. package/dist/adapters/node.js +298 -0
  9. package/dist/adapters/serve.d.ts +12 -0
  10. package/dist/admin/adapters.d.ts +35 -0
  11. package/dist/admin/index.d.ts +50 -0
  12. package/dist/admin/index.js +214 -0
  13. package/dist/app-router/dev-error.d.ts +15 -0
  14. package/dist/app-router/image.d.ts +1 -0
  15. package/dist/app-router/index.d.ts +40 -0
  16. package/dist/app-router/index.js +489 -0
  17. package/dist/app-router/metadata.d.ts +51 -0
  18. package/dist/app-router/scan.d.ts +20 -0
  19. package/dist/app.d.ts +230 -0
  20. package/dist/auth/index.d.ts +65 -0
  21. package/dist/auth/index.js +334 -0
  22. package/dist/auth/jwt.d.ts +13 -0
  23. package/dist/auth/providers.d.ts +43 -0
  24. package/dist/auth/totp.d.ts +35 -0
  25. package/dist/body.d.ts +10 -0
  26. package/dist/chunk-4NOVAJBA.js +499 -0
  27. package/dist/chunk-6VHSSIDV.js +186 -0
  28. package/dist/chunk-FU36MTH6.js +83 -0
  29. package/dist/chunk-H6BWMS4P.js +87 -0
  30. package/dist/chunk-IJNNQKTG.js +626 -0
  31. package/dist/chunk-LAFHS4ZI.js +45 -0
  32. package/dist/chunk-MGKXQINB.js +45 -0
  33. package/dist/chunk-SNAWC6GY.js +123 -0
  34. package/dist/chunk-U626USOW.js +72 -0
  35. package/dist/chunk-VC57IKQN.js +268 -0
  36. package/dist/chunk-Y3YIWI5S.js +217 -0
  37. package/dist/chunk-Z52Z3E6A.js +122 -0
  38. package/dist/client/index.d.ts +91 -0
  39. package/dist/client/index.js +228 -0
  40. package/dist/client/socket.d.ts +46 -0
  41. package/dist/components/index.d.ts +50 -0
  42. package/dist/components/index.js +4 -0
  43. package/dist/context-7E3DE2UC.js +3 -0
  44. package/dist/context.d.ts +47 -0
  45. package/dist/cookie.d.ts +39 -0
  46. package/dist/error.d.ts +34 -0
  47. package/dist/i18n/index.d.ts +36 -0
  48. package/dist/i18n/index.js +47 -0
  49. package/dist/image-FBZPWI2W.js +41 -0
  50. package/dist/index.d.ts +23 -0
  51. package/dist/index.js +23 -0
  52. package/dist/jsx/jsx-dev-runtime.d.ts +1 -0
  53. package/dist/jsx/jsx-dev-runtime.js +1 -0
  54. package/dist/jsx/jsx-runtime.d.ts +1 -0
  55. package/dist/jsx/jsx-runtime.js +1 -0
  56. package/dist/jsx/runtime.d.ts +75 -0
  57. package/dist/mail/index.d.ts +88 -0
  58. package/dist/mail/index.js +147 -0
  59. package/dist/p2p/client.d.ts +26 -0
  60. package/dist/p2p/index.d.ts +1 -0
  61. package/dist/p2p/index.js +39 -0
  62. package/dist/p2p/server.d.ts +16 -0
  63. package/dist/p2p-client.d.ts +2 -0
  64. package/dist/p2p-client.js +88 -0
  65. package/dist/pipeline.d.ts +36 -0
  66. package/dist/plugins/index.d.ts +7 -0
  67. package/dist/plugins/index.js +254 -0
  68. package/dist/plugins/openapi.d.ts +27 -0
  69. package/dist/plugins/ops.d.ts +60 -0
  70. package/dist/plugins/security.d.ts +67 -0
  71. package/dist/plugins/static.d.ts +16 -0
  72. package/dist/react/index.d.ts +36 -0
  73. package/dist/react/index.js +72 -0
  74. package/dist/response-FIW5C377.js +5 -0
  75. package/dist/response.d.ts +23 -0
  76. package/dist/router.d.ts +31 -0
  77. package/dist/schema.d.ts +149 -0
  78. package/dist/serve-GGEVATIS.js +101 -0
  79. package/dist/server/request-store.d.ts +48 -0
  80. package/dist/server/request-store.js +2 -0
  81. package/dist/static-4LGL4QSH.js +8 -0
  82. package/dist/types.d.ts +112 -0
  83. package/dist/upgrade-HCGQKTSH.js +8 -0
  84. package/dist/vite/index.d.ts +19 -0
  85. package/dist/vite/index.js +41 -0
  86. package/dist/vue/index.d.ts +29 -0
  87. package/dist/vue/index.js +55 -0
  88. package/dist/ws/connection.d.ts +37 -0
  89. package/dist/ws/crypto.d.ts +24 -0
  90. package/dist/ws/hub.d.ts +11 -0
  91. package/dist/ws/types.d.ts +47 -0
  92. package/dist/ws/upgrade.d.ts +9 -0
  93. package/package.json +174 -3
package/README.md CHANGED
@@ -1,3 +1,508 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ <a href="https://kwy404.github.io/Raiko.js/">
4
+ <picture>
5
+ <source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/kwy404/Raiko.js@main/assets/banner-dark.svg">
6
+ <img src="https://cdn.jsdelivr.net/gh/kwy404/Raiko.js@main/assets/banner.svg" alt="Raiko.js: backend and frontend in one file tree, every runtime" width="820">
7
+ </picture>
8
+ </a>
9
+
10
+ ### Backend and frontend in one file tree. Every runtime.
11
+
12
+ **A TypeScript framework with a Next.js style app router, server components, a typed API, encrypted WebSockets, auth with 2FA, an admin panel and email. Runs on Bun, Node, Deno, Cloudflare Workers and Vercel.**
13
+
14
+ [![npm](https://img.shields.io/npm/v/raiko.js?color=FFD23F&labelColor=0a0a0b&label=npm)](https://www.npmjs.com/package/raiko.js)
15
+ [![CI](https://github.com/kwy404/Raiko.js/actions/workflows/ci.yml/badge.svg)](https://github.com/kwy404/Raiko.js/actions/workflows/ci.yml)
16
+ [![downloads](https://img.shields.io/npm/dm/raiko.js?color=FFD23F&labelColor=0a0a0b)](https://www.npmjs.com/package/raiko.js)
17
+ [![license](https://img.shields.io/badge/license-MIT-FFD23F?labelColor=0a0a0b)](LICENSE)
18
+ [![types](https://img.shields.io/badge/TypeScript-strict-3178C6?labelColor=0a0a0b)](https://kwy404.github.io/Raiko.js/docs/)
19
+
20
+ **[Website](https://kwy404.github.io/Raiko.js/) · [Docs](https://kwy404.github.io/Raiko.js/docs/) · [Benchmarks](#benchmarks) · [Compare](#how-it-compares) · [GitHub](https://github.com/kwy404/Raiko.js) · [Leia em Português](README.pt-BR.md) · [Leer en Español](README.es.md)**
21
+
22
+ </div>
23
+
24
+ ## Start in one command
25
+
26
+ ```bash
27
+ npx create-raiko@latest my-app
28
+ # or
29
+ bun create raiko my-app
30
+ ```
31
+
32
+ The installer asks what you want and writes a project that already works: frontend (Raiko Pages with Voodoo.js, React, Vue, Svelte, Solid, Preact or Angular), CSS (Monsoon, Tailwind, StyleX, UnoCSS), icons, database (Kastor, Prisma or Drizzle on SQLite or PostgreSQL), sign in (email and password, GitHub, Google, Discord, GitLab), two factor auth, email provider, translations, admin roles, a cookie banner, an installable app, and a deploy target. Every combination is installed, type checked, built and booted in CI.
33
+
34
+ ```bash
35
+ cd my-app
36
+ npm run dev
37
+ ```
38
+
39
+ ## The 30 second tour
40
+
41
+ An API with validation, types and OpenAPI docs from one schema:
42
+
43
+ ```ts
44
+ import { Raiko, t } from 'raiko.js'
45
+
46
+ const app = new Raiko()
47
+ .get('/', 'Hello')
48
+ .get('/users/:id', ({ params }) => ({ id: params.id }))
49
+ .post('/users', ({ body, status }) => status(201, { id: 1, ...body }), {
50
+ body: t.Object({ email: t.String({ format: 'email' }), name: t.String({ minLength: 2 }) }),
51
+ })
52
+
53
+ app.listen(3000)
54
+ export default app
55
+ ```
56
+
57
+ A page is an async server component. It reads the database and ships no JavaScript unless you ask for it:
58
+
59
+ ```tsx
60
+ // app/blog/[slug]/page.tsx
61
+ import { notFound } from 'raiko.js/server'
62
+ import { db } from '../../../db'
63
+
64
+ export const revalidate = 60
65
+ export async function generateMetadata({ params }) {
66
+ return { title: params.slug }
67
+ }
68
+
69
+ export default async function Post({ params }) {
70
+ const post = await db.posts.findUnique({ where: { slug: params.slug } })
71
+ if (!post) notFound()
72
+ return <article><h1>{post.title}</h1><p>{post.body}</p></article>
73
+ }
74
+ ```
75
+
76
+ The client knows every route, param, body and response:
77
+
78
+ ```ts
79
+ import { client } from 'raiko.js/client'
80
+ import type { App } from './server'
81
+
82
+ const api = client<App>('https://api.example.com')
83
+ const { data, error } = await api.post('/users', { body: { email: 'ana@raiko.dev', name: 'Ana' } })
84
+ // ^ { id: number; email: string; name: string } error.status is 422 with the exact field
85
+ ```
86
+
87
+ ## Contents
88
+
89
+ - [Why Raiko](#why-raiko)
90
+ - [Routing and handlers](#routing-and-handlers)
91
+ - [Validation](#validation)
92
+ - [Lifecycle hooks](#lifecycle-hooks)
93
+ - [State, decorators and plugins](#state-decorators-and-plugins)
94
+ - [Responses and streaming](#responses-and-streaming)
95
+ - [App router](#app-router)
96
+ - [Protected pages](#protected-pages)
97
+ - [Components](#components)
98
+ - [Typed client and hooks](#typed-client-and-hooks)
99
+ - [WebSockets](#websockets)
100
+ - [Auth and 2FA](#auth-and-2fa)
101
+ - [Admin panel](#admin-panel)
102
+ - [Email](#email)
103
+ - [Translations](#translations)
104
+ - [Built in plugins](#built-in-plugins)
105
+ - [Errors that tell you the fix](#errors-that-tell-you-the-fix)
106
+ - [Any frontend](#any-frontend)
107
+ - [Deploy](#deploy)
108
+ - [Security](#security)
109
+ - [Benchmarks](#benchmarks)
110
+ - [How it compares](#how-it-compares)
111
+ - [Next.js pain points it solves](#nextjs-pain-points-it-solves)
112
+ - [Testing](#testing)
113
+
114
+ ## Why Raiko
115
+
116
+ | | |
117
+ | --- | --- |
118
+ | **App router like Next.js** | `page`, `layout`, `loading`, `error`, `not-found`, `forbidden`, `unauthorized`, `route` and `socket` files, metadata, form actions and ISR. |
119
+ | **Server components without React** | Async TSX rendered to a string or a stream with `<Suspense>`. Zero client JavaScript by default. |
120
+ | **Interactivity without a build** | Put `v-data` and `v-on:click` in your TSX and Voodoo.js is added to that page only. |
121
+ | **Types from server to client** | Params, query, body and every response status flow into `client<App>()`. No codegen. |
122
+ | **WebSockets on every runtime** | Bun, Node (its own RFC 6455 implementation, no dependency), Deno and Workers, with rooms and validation. |
123
+ | **Encrypted messages** | `encrypt: true` adds ECDH key exchange and AES-GCM with one key per direction and replay protection. |
124
+ | **P2P** | WebRTC signaling and a mesh client for data channels and media. |
125
+ | **Auth** | OAuth with PKCE (GitHub, Google, Discord, GitLab, any OAuth 2), credentials with PBKDF2, TOTP 2FA, signed JWT sessions. |
126
+ | **Admin panel** | CRUD, search, sort and pagination with permissions per role and action, for Kastor, Prisma and Drizzle. |
127
+ | **Email** | Templates in TSX with a live preview, sent through Resend, Postmark, SendGrid or SMTP. |
128
+ | **Errors like Rust** | Every error has a code, the file and line, the source frame and a `help:` line. |
129
+ | **Fast** | Compiled fast paths per route and Bun's native router. See [benchmarks](#benchmarks). |
130
+ | **Tested to break** | 107 tests, over 3.4 million generated property checks, two security audits, 11 templates end to end. |
131
+
132
+ ## Routing and handlers
133
+
134
+ ```ts
135
+ new Raiko()
136
+ .get('/', 'static text') // constant: served by Bun's native router, no JS per request
137
+ .get('/users/:id', ({ params }) => params.id) // params are typed from the path
138
+ .get('/files/*', ({ params }) => params['*']) // wildcard
139
+ .get('/archive/:year?', ({ params }) => params.year ?? 'all')
140
+ .group('/v1', (v1) => v1.get('/health', 'ok'))
141
+ .all('/echo', ({ request }) => request.method)
142
+ .route('PROPFIND', '/dav', () => 'custom method')
143
+ .mount('/legacy', otherFetchHandler) // mount Hono, another app, any fetch handler
144
+ ```
145
+
146
+ The router is a radix tree with a hash map in front for static paths, backtracking so the most specific route wins, automatic `HEAD`, `OPTIONS`, and `405` with an `Allow` header.
147
+
148
+ ## Validation
149
+
150
+ `t` builds schemas with full inference, coerces strings from params, query and forms, and reports the exact path of every problem:
151
+
152
+ ```ts
153
+ import { t } from 'raiko.js'
154
+
155
+ const User = t.Object({
156
+ email: t.String({ format: 'email' }),
157
+ age: t.Optional(t.Integer({ minimum: 13 })),
158
+ tags: t.Array(t.String(), { maxItems: 10 }),
159
+ role: t.Enum(['user', 'admin']),
160
+ })
161
+ ```
162
+
163
+ Zod, Valibot and ArkType work too, through Standard Schema: pass them wherever a `t` schema goes.
164
+
165
+ A failed validation answers `422` with RFC 9457 problem details:
166
+
167
+ ```json
168
+ { "status": 422, "detail": "body.email: Expected a valid email", "errors": [{ "path": ["email"], "on": "body" }] }
169
+ ```
170
+
171
+ ## Lifecycle hooks
172
+
173
+ `onRequest`, `onParse`, `onTransform`, `derive`, validation, `resolve`, `onBeforeHandle`, handler, `onAfterHandle`, `mapResponse`, `onAfterResponse` and `onError`. A hook applies to the routes registered after it. Pass `'global'` to also apply it to the app that uses your plugin.
174
+
175
+ ```ts
176
+ app
177
+ .derive(({ headers }) => ({ token: headers.authorization?.slice(7) }))
178
+ .onBeforeHandle(({ token }) => (token ? undefined : new Response('Unauthorized', { status: 401 })))
179
+ .get('/me', ({ token }) => ({ token }))
180
+ ```
181
+
182
+ ## State, decorators and plugins
183
+
184
+ ```ts
185
+ const db = new Raiko({ name: 'db' }).decorate('db', createDb()).state('hits', 0)
186
+
187
+ const app = new Raiko()
188
+ .use(db) // deduplicated by name
189
+ .get('/hits', ({ store }) => ++store.hits)
190
+ .get('/users', ({ db }) => db.users.findMany())
191
+ ```
192
+
193
+ ## Responses and streaming
194
+
195
+ Return anything: objects become JSON, strings become text, JSX becomes HTML, generators become Server Sent Events, `Blob`, `File`, `ReadableStream` and `Response` pass through. `status(201, body)` sets the code and types the client per status.
196
+
197
+ ```ts
198
+ import { sse } from 'raiko.js'
199
+
200
+ app.get('/ticks', async function* () {
201
+ for (let i = 0; i < 5; i++) yield sse({ i }, { event: 'tick' })
202
+ })
203
+ ```
204
+
205
+ ## App router
206
+
207
+ ```
208
+ app/
209
+ layout.tsx root layout, metadata
210
+ page.tsx /
211
+ loading.tsx streamed fallback while pages load
212
+ error.tsx error boundary
213
+ not-found.tsx 404
214
+ forbidden.tsx 403
215
+ blog/[slug]/page.tsx /blog/:slug
216
+ docs/[...path]/page.tsx
217
+ (marketing)/about/page.tsx route group, not in the URL
218
+ api/users/route.ts export GET, POST...
219
+ chat/socket.ts WebSocket
220
+ middleware.ts
221
+ ```
222
+
223
+ ```ts
224
+ import { Raiko } from 'raiko.js'
225
+ import { appRouter } from 'raiko.js/app'
226
+
227
+ const app = new Raiko().use(await appRouter({ dir: './app' }))
228
+ ```
229
+
230
+ Pages get `params` and `searchParams`, export `metadata` or `generateMetadata`, `revalidate` for cached rendering, and `actions` for forms (`<Form action="like">` posts to `?/like`). Inside any component: `headers()`, `cookies()`, `usePathname()`, `useParams()`, `useSearchParams()`, `currentUser()`, `notFound()`, `redirect()`, `forbidden()`, `unauthorized()`.
231
+
232
+ `sitemap.xml` and `robots.txt` are generated from your pages. In development every page gets an error overlay, a DevTools badge (route, params, render time, cache) and an accessibility checker.
233
+
234
+ ## Protected pages
235
+
236
+ ```tsx
237
+ // app/dashboard/page.tsx
238
+ export const auth = true // signed in users only
239
+ export default function Dashboard() { ... }
240
+
241
+ // app/admin/layout.tsx
242
+ export const roles = ['admin'] // every page under app/admin
243
+ ```
244
+
245
+ ```ts
246
+ appRouter({ getUser: async (ctx) => (await auth.getSession(ctx.request))?.user })
247
+ ```
248
+
249
+ Signed out users go to `/login?callbackUrl=...` (or your `unauthorized.tsx`). Users without the role get `forbidden.tsx` with status 403.
250
+
251
+ ## Components
252
+
253
+ `<Link>` with prefetch, `<Image>` with `srcset`, lazy loading and resizing through sharp when it is installed, `<Script>` with loading strategies, `<Head>`, `<JsonLd>`, `<SkipLink>` and `<Form>`.
254
+
255
+ ```tsx
256
+ import { Image, Link, JsonLd } from 'raiko.js/components'
257
+
258
+ <Image src="/hero.png" alt="Product screenshot" width={1200} height={630} priority />
259
+ ```
260
+
261
+ ## Typed client and hooks
262
+
263
+ ```ts
264
+ const api = client<App>('', { retry: 2, timeout: 10_000, headers: () => ({ authorization: `Bearer ${token}` }) })
265
+ ```
266
+
267
+ React and Vue hooks for queries, mutations and sockets:
268
+
269
+ ```tsx
270
+ import { useQuery, useSocket } from 'raiko.js/react' // or 'raiko.js/vue'
271
+
272
+ const users = useQuery(() => api.get('/users'), { key: 'users' })
273
+ const chat = useSocket(() => api.ws('/chat'), (msg) => setMessages((m) => [...m, msg]))
274
+ ```
275
+
276
+ ## WebSockets
277
+
278
+ ```ts
279
+ app.ws('/chat/:room', {
280
+ body: t.Object({ text: t.String({ maxLength: 500 }) }),
281
+ encrypt: true,
282
+ open: (ws) => ws.subscribe(ws.data.params.room),
283
+ message: (ws, msg) => ws.publish(ws.data.params.room, { from: ws.id, text: msg.text }),
284
+ })
285
+ ```
286
+
287
+ The browser client reconnects with backoff, queues messages while offline and handles the key exchange. With `encrypt: true`, keys are agreed with ECDH P-256, each direction gets its own AES-GCM 256 key from HKDF, and nonces are frame counters, so replayed, reordered or reflected frames are refused. This runs on top of TLS (`wss://`); it keeps message content away from proxies and logs, not from the user's own browser.
288
+
289
+ P2P with WebRTC:
290
+
291
+ ```ts
292
+ import { signaling } from 'raiko.js/p2p'
293
+ app.use(signaling({ path: '/p2p/:room' }))
294
+
295
+ // browser
296
+ import { joinRoom } from 'raiko.js/p2p/client'
297
+ const room = joinRoom(api.ws('/p2p/lobby'))
298
+ room.on('message', (from, data) => console.log(from, data))
299
+ room.send({ hi: true })
300
+ ```
301
+
302
+ ## Auth and 2FA
303
+
304
+ ```ts
305
+ import { raikoAuth, GitHub, Google, Credentials, verifyPassword } from 'raiko.js/auth'
306
+
307
+ export const auth = raikoAuth({
308
+ secret: process.env.AUTH_SECRET!,
309
+ providers: [
310
+ GitHub({ clientId, clientSecret }),
311
+ Credentials({ authorize: async ({ email, password }) => /* your lookup */ null }),
312
+ ],
313
+ twoFactor: { getSecret: (user) => user.totpSecret },
314
+ })
315
+
316
+ app.use(auth.plugin).get('/me', ({ auth }) => auth?.user, { beforeHandle: auth.protect })
317
+ ```
318
+
319
+ OAuth uses PKCE and a signed state. Passwords use PBKDF2 SHA-256 with 210,000 iterations. TOTP works with Google Authenticator, Authy and 1Password, allows five tries per sign in and refuses a reused code. `generateTotpSecret()`, `totpUri()` and `recoveryCodes()` help you build the setup screen.
320
+
321
+ ## Admin panel
322
+
323
+ ```ts
324
+ import { admin, modelAdapter } from 'raiko.js/admin'
325
+
326
+ app.use(admin({
327
+ auth: ({ auth }) => auth?.user.role ?? null,
328
+ resources: {
329
+ users: {
330
+ adapter: modelAdapter(db.users, { searchable: ['email'] }),
331
+ fields: ['id', 'email', 'name', 'role'],
332
+ hidden: ['passwordHash'],
333
+ permissions: { admin: '*', support: ['read', 'update'], viewer: ['list'] },
334
+ },
335
+ },
336
+ }))
337
+ ```
338
+
339
+ Each action (`list`, `read`, `create`, `update`, `delete`) is checked on its own, so a support role can fix a user without being able to list or delete them. `modelAdapter` works with Kastor and Prisma, `drizzleAdapter` with Drizzle.
340
+
341
+ ## Email
342
+
343
+ ```tsx
344
+ // emails/welcome.tsx
345
+ /** @jsxImportSource raiko.js */
346
+ export default function Welcome({ name }: { name: string }) {
347
+ return <h1>Welcome, {name}</h1>
348
+ }
349
+ Welcome.subject = (p) => `Welcome, ${p.name}`
350
+ Welcome.preview = { name: 'Ana' }
351
+ ```
352
+
353
+ ```ts
354
+ import { createMailer, resend } from 'raiko.js/mail'
355
+
356
+ const mailer = createMailer({ from: 'hi@my.app', transport: resend({ apiKey }), templates: { welcome: Welcome } })
357
+ await mailer.send({ to: 'ana@x.io', template: 'welcome', props: { name: 'Ana' } })
358
+ app.use(mailer.preview()) // /_raiko/mail lists and renders every template
359
+ ```
360
+
361
+ A plain text version is generated for every email. Templates can also return HTML from React Email, Vue or any string.
362
+
363
+ ## Translations
364
+
365
+ ```ts
366
+ import { createI18n } from 'raiko.js/i18n'
367
+
368
+ const i18n = createI18n({ en, 'pt-BR': ptBR }, { fallback: 'en', cookie: 'locale' })
369
+ i18n.t('pt-BR', 'cart.items', { count: 3 }) // "3 itens", plurals with Intl.PluralRules
370
+ ```
371
+
372
+ Keys are typed from your fallback dictionary. Locale detection reads the URL prefix, a cookie and `Accept-Language`.
373
+
374
+ ## Built in plugins
375
+
376
+ From `raiko.js/plugins`: `cors`, `secureHeaders`, `rateLimit`, `csrf`, `logger`, `requestId`, `compress`, `etag`, `responseCache`, `health`, `staticFiles` and `openapi` (OpenAPI 3.1 with a docs page at `/docs`).
377
+
378
+ ## Errors that tell you the fix
379
+
380
+ ```
381
+ error[RK0104]: two routes use different param names at the same position
382
+ --> app/blog/[slug]/page.tsx
383
+ |
384
+ 3 | app/blog/[id]/page.tsx
385
+ | ^^^^ already declared as [slug]
386
+ |
387
+ = help: rename the folder to [slug], or move one of the pages
388
+ ```
389
+
390
+ Every coded error (`RK0100` and up) has a page in the [error reference](https://kwy404.github.io/Raiko.js/docs/errors.html).
391
+
392
+ ## Any frontend
393
+
394
+ Raiko Pages render on the server. For a single page app, the Vite plugin runs the backend inside the Vite dev server, so React, Vue, Svelte, Solid, Preact or Angular share one port with the API:
395
+
396
+ ```ts
397
+ // vite.config.ts
398
+ import react from '@vitejs/plugin-react'
399
+ import { raiko } from 'raiko.js/vite'
400
+
401
+ export default { plugins: [react(), raiko({ entry: './server/index.ts' })] }
402
+ ```
403
+
404
+ Production builds are minified and mangled, with no source maps. Obfuscation slows down reading, it does not stop it: secrets and rules stay on the server.
405
+
406
+ ## Deploy
407
+
408
+ | Target | How |
409
+ | --- | --- |
410
+ | Bun, Node, Deno | `app.listen(3000)`, or `raiko build` then `raiko start` |
411
+ | Many cores | `app.listen(3000, { cluster: true })`: one process per core on the same port |
412
+ | Vercel | `export default { fetch: app.fetch }` in `api/index.ts` (the installer writes it) |
413
+ | Cloudflare Workers | `export default { fetch: app.fetch }`, `wrangler deploy` |
414
+ | Docker | the installer writes a non root, multi stage `Dockerfile` |
415
+
416
+ `raiko deploy` picks the target from your project.
417
+
418
+ ## Security
419
+
420
+ Defaults that hold under attack, each one backed by a test:
421
+
422
+ - HTML is escaped everywhere, attribute and tag names are validated.
423
+ - Query, cookie and form parsing use null prototype objects (no prototype pollution).
424
+ - Static files and the image endpoint refuse path traversal.
425
+ - Cookies can be signed, signatures have one canonical encoding.
426
+ - `cors({ credentials: true })` refuses to run without an explicit origin list.
427
+ - Redirects after sign in and after form actions stay on your site.
428
+ - Body limits apply to chunked and multipart uploads.
429
+ - Response validation errors never send server data to the client.
430
+ - Caches never serve one user's response to another.
431
+ - The rate limiter does not trust `X-Forwarded-For` unless you say so.
432
+
433
+ Two independent audits found 21 issues before this release. All are fixed, and each has a test that reproduces the attack. Report a vulnerability through [SECURITY.md](SECURITY.md).
434
+
435
+ ## Benchmarks
436
+
437
+ Requests per second, higher is better. The same three routes in every framework, on Bun, over real HTTP with autocannon (64 connections, pipelining 4), median of three rounds.
438
+
439
+ <!-- bench:start -->
440
+ | Server rendered page | Raiko.js (Bun) | Raiko.js (Node) | Next.js 15 (Node) |
441
+ | --- | ---: | ---: | ---: |
442
+ | req/s | 8,910 | 3,737 | 155 |
443
+
444
+ | Route | Raiko.js | Elysia 1.4 | Hono 4.13 |
445
+ | --- | ---: | ---: | ---: |
446
+ | text | 99,040 | 83,192 | 63,920 |
447
+ | params + json | 66,943 | 75,723 | 60,477 |
448
+ | json body | 51,136 | 53,168 | 47,029 |
449
+
450
+ AMD EPYC 9J45, 1 vCPU, 1.9 GB RAM, Linux 6.8, Bun 1.4.4, Node 22.23. autocannon -c 64 -p 4, median of 3 rounds.
451
+ <!-- bench:end -->
452
+
453
+ Honest notes: the machine has one vCPU, so the load generator and the server share it and the numbers are compressed. On static routes, Raiko and Elysia both answer from Bun's native router without running JavaScript, so they tie at the ceiling. Run `bun bench/http.ts` on your own hardware.
454
+
455
+ ## How it compares
456
+
457
+ | | Raiko.js | Elysia | Next.js | Hono |
458
+ | --- | --- | --- | --- | --- |
459
+ | Runtimes | Bun, Node, Deno, Workers, Vercel | Bun first | Node, Vercel | All |
460
+ | App router, layouts, loading, error | Yes | No | Yes | No |
461
+ | Server components | Yes, no React needed | No | Yes | JSX only |
462
+ | Typed client | Yes | Yes (Eden) | Server actions | Yes (RPC) |
463
+ | WebSockets on every runtime | Yes | Bun | No | Per adapter |
464
+ | Encrypted WebSocket messages | Yes | No | No | No |
465
+ | Auth with OAuth and 2FA | Built in | Plugins | NextAuth | Middleware |
466
+ | Admin panel with permissions | Built in | No | No | No |
467
+ | Email templates with preview | Built in | No | No | No |
468
+ | Ecosystem size | New | Growing | Huge | Large |
469
+
470
+ ## Next.js pain points it solves
471
+
472
+ - **One process for pages and the API.** No separate backend, no duplicated types.
473
+ - **No React required on the server.** Pages are plain async functions that return TSX.
474
+ - **Real WebSockets.** Next.js has no WebSocket server, and Raiko has one on every runtime.
475
+ - **Predictable caching.** Only pages that export `revalidate` are cached, never personalized requests.
476
+ - **Runs anywhere.** The same app deploys to Bun, Node, Deno, Workers and Vercel without an adapter project.
477
+ - **Errors you can act on.** Coded errors with the fix, not a minified stack trace.
478
+ - **Auth, admin and email included.** The things every product needs are not separate libraries to wire.
479
+
480
+ ## Testing
481
+
482
+ ```ts
483
+ const res = await app.request('/users', { json: { email: 'a@b.co', name: 'Ana' } })
484
+ expect(res.status).toBe(201)
485
+ ```
486
+
487
+ No server, no port: `app.request()` and `app.fetch()` run the full lifecycle in memory.
488
+
489
+ ## More projects by kwy404
490
+
491
+ | Project | What it does |
492
+ | --- | --- |
493
+ | [Voodoo.js](https://github.com/kwy404/Voodoo.js) | JSX in plain HTML with no build step, used for interactivity in Raiko Pages |
494
+ | [Monsoon](https://github.com/kwy404/monsoon) | Utility first CSS with no build step, a theme from one color |
495
+ | [glyphfox](https://github.com/kwy404/glyphfox) | Morphable icons for every framework |
496
+ | [Kastor](https://github.com/kwy404/kastor) | The TypeScript ORM for almost every database |
497
+ | [a11ymedic](https://github.com/kwy404/a11ymedic) | Accessibility checker that explains the fix |
498
+ | [cookieowl](https://github.com/kwy404/cookieowl) | Cookie consent for LGPD and GDPR that really blocks trackers |
499
+ | [installkite](https://github.com/kwy404/installkite) | Make any website installable and offline capable |
500
+ | [edgegoat](https://github.com/kwy404/edgegoat) | Edge case test inputs for JavaScript |
501
+
502
+ ## Author
503
+
504
+ Created by **Alexandre Kohler** ([GitHub](https://github.com/kwy404)).
505
+
506
+ ## License
507
+
508
+ [MIT](LICENSE)