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.
- package/LICENSE +21 -0
- package/README.es.md +508 -0
- package/README.md +507 -2
- package/README.pt-BR.md +508 -0
- package/bin/raiko.mjs +149 -0
- package/dist/adapters/node-ws.d.ts +40 -0
- package/dist/adapters/node.d.ts +6 -0
- package/dist/adapters/node.js +298 -0
- package/dist/adapters/serve.d.ts +12 -0
- package/dist/admin/adapters.d.ts +35 -0
- package/dist/admin/index.d.ts +50 -0
- package/dist/admin/index.js +214 -0
- package/dist/app-router/dev-error.d.ts +15 -0
- package/dist/app-router/image.d.ts +1 -0
- package/dist/app-router/index.d.ts +40 -0
- package/dist/app-router/index.js +489 -0
- package/dist/app-router/metadata.d.ts +51 -0
- package/dist/app-router/scan.d.ts +20 -0
- package/dist/app.d.ts +230 -0
- package/dist/auth/index.d.ts +65 -0
- package/dist/auth/index.js +334 -0
- package/dist/auth/jwt.d.ts +13 -0
- package/dist/auth/providers.d.ts +43 -0
- package/dist/auth/totp.d.ts +35 -0
- package/dist/body.d.ts +10 -0
- package/dist/chunk-4NOVAJBA.js +499 -0
- package/dist/chunk-6VHSSIDV.js +186 -0
- package/dist/chunk-FU36MTH6.js +83 -0
- package/dist/chunk-H6BWMS4P.js +87 -0
- package/dist/chunk-IJNNQKTG.js +626 -0
- package/dist/chunk-LAFHS4ZI.js +45 -0
- package/dist/chunk-MGKXQINB.js +45 -0
- package/dist/chunk-SNAWC6GY.js +123 -0
- package/dist/chunk-U626USOW.js +72 -0
- package/dist/chunk-VC57IKQN.js +268 -0
- package/dist/chunk-Y3YIWI5S.js +217 -0
- package/dist/chunk-Z52Z3E6A.js +122 -0
- package/dist/client/index.d.ts +91 -0
- package/dist/client/index.js +228 -0
- package/dist/client/socket.d.ts +46 -0
- package/dist/components/index.d.ts +50 -0
- package/dist/components/index.js +4 -0
- package/dist/context-7E3DE2UC.js +3 -0
- package/dist/context.d.ts +47 -0
- package/dist/cookie.d.ts +39 -0
- package/dist/error.d.ts +34 -0
- package/dist/i18n/index.d.ts +36 -0
- package/dist/i18n/index.js +47 -0
- package/dist/image-FBZPWI2W.js +41 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +23 -0
- package/dist/jsx/jsx-dev-runtime.d.ts +1 -0
- package/dist/jsx/jsx-dev-runtime.js +1 -0
- package/dist/jsx/jsx-runtime.d.ts +1 -0
- package/dist/jsx/jsx-runtime.js +1 -0
- package/dist/jsx/runtime.d.ts +75 -0
- package/dist/mail/index.d.ts +88 -0
- package/dist/mail/index.js +147 -0
- package/dist/p2p/client.d.ts +26 -0
- package/dist/p2p/index.d.ts +1 -0
- package/dist/p2p/index.js +39 -0
- package/dist/p2p/server.d.ts +16 -0
- package/dist/p2p-client.d.ts +2 -0
- package/dist/p2p-client.js +88 -0
- package/dist/pipeline.d.ts +36 -0
- package/dist/plugins/index.d.ts +7 -0
- package/dist/plugins/index.js +254 -0
- package/dist/plugins/openapi.d.ts +27 -0
- package/dist/plugins/ops.d.ts +60 -0
- package/dist/plugins/security.d.ts +67 -0
- package/dist/plugins/static.d.ts +16 -0
- package/dist/react/index.d.ts +36 -0
- package/dist/react/index.js +72 -0
- package/dist/response-FIW5C377.js +5 -0
- package/dist/response.d.ts +23 -0
- package/dist/router.d.ts +31 -0
- package/dist/schema.d.ts +149 -0
- package/dist/serve-GGEVATIS.js +101 -0
- package/dist/server/request-store.d.ts +48 -0
- package/dist/server/request-store.js +2 -0
- package/dist/static-4LGL4QSH.js +8 -0
- package/dist/types.d.ts +112 -0
- package/dist/upgrade-HCGQKTSH.js +8 -0
- package/dist/vite/index.d.ts +19 -0
- package/dist/vite/index.js +41 -0
- package/dist/vue/index.d.ts +29 -0
- package/dist/vue/index.js +55 -0
- package/dist/ws/connection.d.ts +37 -0
- package/dist/ws/crypto.d.ts +24 -0
- package/dist/ws/hub.d.ts +11 -0
- package/dist/ws/types.d.ts +47 -0
- package/dist/ws/upgrade.d.ts +9 -0
- package/package.json +174 -3
package/README.md
CHANGED
|
@@ -1,3 +1,508 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/raiko.js)
|
|
15
|
+
[](https://github.com/kwy404/Raiko.js/actions/workflows/ci.yml)
|
|
16
|
+
[](https://www.npmjs.com/package/raiko.js)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
[](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)
|