@celsian/adapter-railway 0.3.6 → 0.3.8
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/README.md +596 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
# CelsianJS
|
|
2
|
+
|
|
3
|
+
TypeScript backend framework built on Web Standard APIs. Ships runtime adapters for Node.js, Bun, Deno, Cloudflare Workers, AWS Lambda, and Vercel.
|
|
4
|
+
|
|
5
|
+
- **Multi-runtime** -- Write once for Web Standard `Request`/`Response`, then deploy with the adapter that matches your target runtime.
|
|
6
|
+
- **Significantly faster than Express** -- Radix-tree router, zero-copy request building, pre-stringified error paths. 1.3x-1.7x faster across all scenarios.
|
|
7
|
+
- **First-party batteries** -- Background tasks, cron, WebSocket, CORS, CSRF protection, security headers, DB analytics, rate limiting, JWT, caching, compression, and OpenAPI docs via core APIs plus first-party packages.
|
|
8
|
+
- **Fastify-style plugin encapsulation** -- Scoped hooks and decorations by default. No accidental middleware leaks.
|
|
9
|
+
- **Schema-agnostic validation** -- Auto-detects Zod, TypeBox, or Valibot. No config, no adapters.
|
|
10
|
+
|
|
11
|
+
## Local pnpm note
|
|
12
|
+
|
|
13
|
+
This monorepo is pinned to `pnpm@9.15.0`. For local release verification, prefer:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm run setup:pnpm
|
|
17
|
+
# or, if Corepack is unavailable locally:
|
|
18
|
+
npx -y pnpm@9.15.0 <script>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
CI and release gates should use the pinned package manager version, not an older global pnpm.
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npx create-celsian my-api
|
|
27
|
+
cd my-api
|
|
28
|
+
npm install
|
|
29
|
+
npm run dev
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or manually for the core router/runtime:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @celsian/core
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { createApp, serve } from '@celsian/core';
|
|
40
|
+
|
|
41
|
+
const app = createApp({ logger: true });
|
|
42
|
+
|
|
43
|
+
// ─── Background tasks with retries ───
|
|
44
|
+
app.task({
|
|
45
|
+
name: 'sendWelcomeEmail',
|
|
46
|
+
retries: 3,
|
|
47
|
+
async handler(input: { to: string }) {
|
|
48
|
+
await sendEmail(input.to, 'Welcome!');
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// ─── Cron job: clean up expired sessions every night ───
|
|
53
|
+
app.cron('cleanup', '0 3 * * *', async () => {
|
|
54
|
+
await db.query('DELETE FROM sessions WHERE expires_at < NOW()');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// ─── API routes ───
|
|
58
|
+
app.post('/users', async (req, reply) => {
|
|
59
|
+
const body = req.parsedBody as { name: string; email: string };
|
|
60
|
+
const user = await db.createUser(body);
|
|
61
|
+
await app.enqueue('sendWelcomeEmail', { to: body.email });
|
|
62
|
+
return reply.status(201).json(user);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
app.get('/users/:id', (req, reply) => {
|
|
66
|
+
return reply.json({ id: req.params.id, name: 'Alice' });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
serve(app, { port: 3000 });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Tasks, cron, and API routes in one file -- no separate worker process needed. On Bun or Deno, `serve()` auto-detects the runtime. No code changes needed.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
bun run server.ts # Uses Bun.serve() automatically
|
|
76
|
+
deno run server.ts # Uses Deno.serve() automatically
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Why CelsianJS
|
|
80
|
+
|
|
81
|
+
### Multi-Runtime
|
|
82
|
+
|
|
83
|
+
Built on Web Standard `Request`/`Response` -- not Node.js `IncomingMessage`/`ServerResponse`. One adapter line deploys anywhere:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
export default createCloudflareHandler(app); // Cloudflare Workers
|
|
87
|
+
export const handler = createLambdaHandler(app); // AWS Lambda
|
|
88
|
+
export default createVercelEdgeHandler(app); // Vercel Edge
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Honest Benchmarks
|
|
92
|
+
|
|
93
|
+
Benchmarked on Node.js v22, Apple Silicon, 10 connections for 10 seconds per scenario:
|
|
94
|
+
|
|
95
|
+
| Scenario | Fastify (req/s) | CelsianJS (req/s) | Express (req/s) |
|
|
96
|
+
| --------------------- | ---------------: | -----------------: | --------------: |
|
|
97
|
+
| JSON response | 45,866 | 27,996 | 16,321 |
|
|
98
|
+
| Route params | 45,440 | 27,026 | 16,288 |
|
|
99
|
+
| Middleware (5 layers) | 41,380 | 24,445 | 15,751 |
|
|
100
|
+
| JSON body parsing | 29,998 | 19,074 | 14,648 |
|
|
101
|
+
| Error handling | 32,398 | 18,542 | 14,765 |
|
|
102
|
+
|
|
103
|
+
**Fastify is faster.** It operates directly on Node.js internals with `fast-json-stringify` — hard to beat. CelsianJS pays a performance tax for Web Standard API compatibility (`Request`/`Response` object creation per request).
|
|
104
|
+
|
|
105
|
+
**CelsianJS is 1.3-1.7x faster than Express** while shipping batteries that neither Fastify nor Express include: background task queues, cron scheduling, multi-runtime deployment, and DB analytics. If raw throughput is your only concern, use Fastify. If you need application infrastructure in a single framework, that's where CelsianJS fits.
|
|
106
|
+
|
|
107
|
+
### Built-In Everything
|
|
108
|
+
|
|
109
|
+
Celsian's batteries ship as first-party packages. Install the extras you use:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm install @celsian/core @celsian/rate-limit @celsian/jwt @celsian/compress
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { createApp, security, cors, csrf, openapi } from '@celsian/core';
|
|
117
|
+
import { rateLimit } from '@celsian/rate-limit';
|
|
118
|
+
import { jwt } from '@celsian/jwt';
|
|
119
|
+
import { compress } from '@celsian/compress';
|
|
120
|
+
|
|
121
|
+
const app = createApp();
|
|
122
|
+
|
|
123
|
+
await app.register(security(), { encapsulate: false }); // Helmet-style headers
|
|
124
|
+
await app.register(cors({ origin: 'https://myapp.com' }));
|
|
125
|
+
await app.register(csrf(), { encapsulate: false }); // CSRF token protection
|
|
126
|
+
await app.register(rateLimit({ max: 100, window: 60_000, keyGenerator: req => req.headers.get("x-api-key") ?? "public" }));
|
|
127
|
+
await app.register(compress());
|
|
128
|
+
await app.register(jwt({ secret: process.env.JWT_SECRET! }));
|
|
129
|
+
await app.register(openapi({ title: 'My API' }));
|
|
130
|
+
|
|
131
|
+
app.health(); // /health + /ready
|
|
132
|
+
app.task({ name: 'email', handler, retries: 3 }); // Background tasks
|
|
133
|
+
app.cron('cleanup', '0 3 * * *', cleanupHandler); // Cron jobs
|
|
134
|
+
app.ws('/chat', { open, message, close }); // WebSocket
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Plugin Encapsulation
|
|
138
|
+
|
|
139
|
+
Plugins get isolated scopes by default. Hooks and decorations registered inside a plugin do not leak to sibling plugins or the parent scope.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// Auth plugin -- hooks only apply to routes registered inside
|
|
143
|
+
async function authPlugin(app) {
|
|
144
|
+
app.addHook('onRequest', async (req, reply) => {
|
|
145
|
+
const token = req.headers.get('authorization');
|
|
146
|
+
if (!token) return reply.unauthorized();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.get('/me', (req, reply) => {
|
|
150
|
+
return reply.json({ user: req.user });
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Public routes -- no auth required
|
|
155
|
+
app.get('/health', (req, reply) => reply.json({ status: 'ok' }));
|
|
156
|
+
|
|
157
|
+
// Register auth plugin under /api prefix
|
|
158
|
+
await app.register(authPlugin, { prefix: '/api' });
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Use `{ encapsulate: false }` when a plugin should affect all routes (e.g., CORS, database):
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
await app.register(cors(), { encapsulate: false });
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Type-Safe Schema Validation
|
|
168
|
+
|
|
169
|
+
Pass any Zod, TypeBox, or Valibot schema. CelsianJS auto-detects the library.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
app.route({
|
|
173
|
+
method: 'POST',
|
|
174
|
+
url: '/users',
|
|
175
|
+
schema: {
|
|
176
|
+
body: z.object({ name: z.string().min(1), email: z.string().email() }),
|
|
177
|
+
},
|
|
178
|
+
handler(req, reply) {
|
|
179
|
+
const { name, email } = req.parsedBody as { name: string; email: string };
|
|
180
|
+
return reply.status(201).json({ id: '1', name, email });
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
// Invalid input returns 400 with structured issues automatically
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### JSX Server Rendering
|
|
187
|
+
|
|
188
|
+
CelsianJS includes a built-in JSX runtime for server-side HTML rendering -- no React dependency needed. Supports both automatic and classic transforms.
|
|
189
|
+
|
|
190
|
+
**Setup (automatic transform -- recommended):**
|
|
191
|
+
|
|
192
|
+
```jsonc
|
|
193
|
+
// tsconfig.json
|
|
194
|
+
{
|
|
195
|
+
"compilerOptions": {
|
|
196
|
+
"jsx": "react-jsx",
|
|
197
|
+
"jsxImportSource": "@celsian/core"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Setup (classic transform):**
|
|
203
|
+
|
|
204
|
+
```jsonc
|
|
205
|
+
// tsconfig.json
|
|
206
|
+
{
|
|
207
|
+
"compilerOptions": {
|
|
208
|
+
"jsx": "react",
|
|
209
|
+
"jsxFactory": "h",
|
|
210
|
+
"jsxFragmentFactory": "Fragment"
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { h, Fragment } from '@celsian/core/jsx';
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Usage:**
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
import { renderToString, renderToDocument } from '@celsian/core';
|
|
223
|
+
|
|
224
|
+
// Function components work like React
|
|
225
|
+
function Layout({ title, children }: { title: string; children: any }) {
|
|
226
|
+
return (
|
|
227
|
+
<html>
|
|
228
|
+
<head><title>{title}</title></head>
|
|
229
|
+
<body>{children}</body>
|
|
230
|
+
</html>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function UserCard({ name, email }: { name: string; email: string }) {
|
|
235
|
+
return (
|
|
236
|
+
<div className="card">
|
|
237
|
+
<h2>{name}</h2>
|
|
238
|
+
<p>{email}</p>
|
|
239
|
+
</div>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
app.get('/page', (req, reply) => {
|
|
244
|
+
const html = renderToDocument(
|
|
245
|
+
<Layout title="Users">
|
|
246
|
+
<UserCard name="Alice" email="alice@example.com" />
|
|
247
|
+
</Layout>
|
|
248
|
+
);
|
|
249
|
+
return reply.html(html);
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Supports `className`/`htmlFor` mapping, style objects, boolean attributes, void elements, `dangerouslySetInnerHTML`, Fragments, and XSS-safe escaping.
|
|
254
|
+
|
|
255
|
+
## Features at a Glance
|
|
256
|
+
|
|
257
|
+
| Category | Features |
|
|
258
|
+
| -------- | -------- |
|
|
259
|
+
| **Routing** | Radix-tree router, params, wildcards, HEAD fallback, 405, route tagging |
|
|
260
|
+
| **Hooks** | 8-hook lifecycle (onRequest through onResponse), route-level hooks |
|
|
261
|
+
| **Plugins** | Scoped encapsulation, app/request/reply decorators |
|
|
262
|
+
| **Validation** | Zod, TypeBox, Valibot auto-detect; body, querystring, params schemas |
|
|
263
|
+
| **Reply** | json, html, stream, redirect, sendFile (Range/ETag/304), download, cookies, 9 error helpers, JSX SSR |
|
|
264
|
+
| **Security** | Helmet-style headers, CORS, CSRF protection, JWT, fixed-window rate limiting |
|
|
265
|
+
| **Background** | Task queue with retries, cron scheduling, Redis queue backend |
|
|
266
|
+
| **Real-time** | WebSocket with broadcast and connection management |
|
|
267
|
+
| **Database** | Connection pool plugin, transactions, query analytics, Server-Timing |
|
|
268
|
+
| **Caching** | Response cache, session management (KV store) |
|
|
269
|
+
| **Infra** | Brotli/gzip/deflate compression, OpenAPI 3.1 + Swagger UI, structured logging, inject() testing |
|
|
270
|
+
| **Deploy** | Node, Bun, Deno, Workers, Lambda, Vercel, Fly.io, Railway, graceful shutdown |
|
|
271
|
+
|
|
272
|
+
## Core Concepts
|
|
273
|
+
|
|
274
|
+
### Routes and Handlers
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
app.get('/users/:id', (req, reply) => {
|
|
278
|
+
return reply.json({ id: req.params.id, include: req.query.include });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Full route options with schema, hooks, and deployment tagging
|
|
282
|
+
app.route({
|
|
283
|
+
method: 'POST',
|
|
284
|
+
url: '/items',
|
|
285
|
+
kind: 'serverless',
|
|
286
|
+
schema: { body: mySchema },
|
|
287
|
+
preHandler: [authHook],
|
|
288
|
+
handler(req, reply) { return reply.status(201).json(req.parsedBody); },
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Hooks Lifecycle
|
|
293
|
+
|
|
294
|
+
8 hooks run in order: `onRequest` > `preParsing` > `preValidation` > `preHandler` > `handler` > `preSerialization` > `onSend` > `onResponse`. Plus `onError` for error handling. Any hook can short-circuit by returning a `Response`.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
// Global hook
|
|
298
|
+
app.addHook('onRequest', async (req, reply) => {
|
|
299
|
+
reply.header('x-request-id', crypto.randomUUID());
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Route-level hook
|
|
303
|
+
app.route({
|
|
304
|
+
method: 'POST',
|
|
305
|
+
url: '/admin/users',
|
|
306
|
+
onRequest: [requireAdmin],
|
|
307
|
+
handler(req, reply) { return reply.json({ created: true }); },
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
See [Hooks Lifecycle](docs/hooks.md) for the complete guide.
|
|
312
|
+
|
|
313
|
+
### Reply Helpers
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
reply.json({ data: [] }); // JSON response
|
|
317
|
+
reply.html('<h1>Hello</h1>'); // HTML response
|
|
318
|
+
reply.stream(readableStream); // Streaming
|
|
319
|
+
reply.redirect('/new-path', 301); // Redirect
|
|
320
|
+
await reply.sendFile('/path/to/report.pdf'); // Serve file
|
|
321
|
+
await reply.download('/path/to/data.csv', 'export'); // Download
|
|
322
|
+
|
|
323
|
+
// Structured error responses
|
|
324
|
+
reply.notFound('User not found'); // 404
|
|
325
|
+
reply.badRequest('Missing email'); // 400
|
|
326
|
+
reply.unauthorized('Token expired'); // 401
|
|
327
|
+
reply.forbidden(); // 403
|
|
328
|
+
reply.conflict(); // 409
|
|
329
|
+
reply.tooManyRequests(); // 429
|
|
330
|
+
|
|
331
|
+
// Cookies + chaining
|
|
332
|
+
reply.cookie('session', token, { httpOnly: true, secure: true });
|
|
333
|
+
return reply.status(201).header('x-custom', 'value').json({ id: '1' });
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
#### File Serving with Range Requests
|
|
337
|
+
|
|
338
|
+
`sendFile` and `download` support Range requests (HTTP 206), conditional GET (304), ETag, and Last-Modified headers. Pass `options.request` to enable these features:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
app.get('/files/:name', async (req, reply) => {
|
|
342
|
+
return reply.sendFile(req.params.name, {
|
|
343
|
+
root: './uploads', // Resolve relative to this directory (with traversal protection)
|
|
344
|
+
request: req, // Enable Range requests + conditional GET (304)
|
|
345
|
+
cacheControl: 'public, max-age=3600', // Set Cache-Control header
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Download with Range support (for resumable downloads)
|
|
350
|
+
app.get('/export/:id', async (req, reply) => {
|
|
351
|
+
return reply.download(`./data/${req.params.id}.csv`, 'export.csv', {
|
|
352
|
+
request: req,
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Supported Range formats: `bytes=0-499`, `bytes=500-`, `bytes=-500`, and multi-range (`bytes=0-499, 1000-1499`). Invalid ranges return 416 Range Not Satisfiable.
|
|
358
|
+
|
|
359
|
+
#### Compression with Brotli
|
|
360
|
+
|
|
361
|
+
The compression plugin prefers Brotli over gzip when the client supports it, providing better compression ratios for text content:
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
import { compress } from '@celsian/compress';
|
|
365
|
+
|
|
366
|
+
// Brotli + gzip + deflate (default)
|
|
367
|
+
await app.register(compress());
|
|
368
|
+
|
|
369
|
+
// Custom Brotli quality (0-11, default 4)
|
|
370
|
+
await app.register(compress({ brotliQuality: 6 }));
|
|
371
|
+
|
|
372
|
+
// Gzip/deflate only (disable Brotli)
|
|
373
|
+
await app.register(compress({ encodings: ['gzip', 'deflate'] }));
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Error Handling
|
|
377
|
+
|
|
378
|
+
Thrown errors are caught and returned as structured JSON. Stack traces are stripped in production.
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
import { HttpError } from '@celsian/core';
|
|
382
|
+
|
|
383
|
+
// Throw HTTP errors anywhere
|
|
384
|
+
throw new HttpError(403, 'Forbidden');
|
|
385
|
+
// { "error": "Forbidden", "statusCode": 403, "code": "FORBIDDEN" }
|
|
386
|
+
|
|
387
|
+
// Custom error handler
|
|
388
|
+
app.setErrorHandler((error, req, reply) => {
|
|
389
|
+
if (error.message.includes('UNIQUE constraint')) return reply.conflict();
|
|
390
|
+
return reply.internalServerError();
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Type-Safe RPC
|
|
395
|
+
|
|
396
|
+
`@celsian/rpc` provides tRPC-style procedures with type inference, middleware, and OpenAPI generation.
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
// server.ts
|
|
400
|
+
import { procedure, router, RPCHandler } from '@celsian/rpc';
|
|
401
|
+
|
|
402
|
+
const appRouter = router({
|
|
403
|
+
users: {
|
|
404
|
+
list: procedure
|
|
405
|
+
.input(z.object({ limit: z.number().optional() }))
|
|
406
|
+
.query(async ({ input }) => [{ id: '1', name: 'Alice' }]),
|
|
407
|
+
create: procedure
|
|
408
|
+
.input(z.object({ name: z.string(), email: z.string().email() }))
|
|
409
|
+
.mutation(async ({ input }) => ({ id: '2', ...input })),
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const rpc = new RPCHandler(appRouter);
|
|
414
|
+
app.route({ method: ['GET', 'POST'], url: '/_rpc/*path', handler: (req) => rpc.handle(req) });
|
|
415
|
+
export type AppRouter = typeof appRouter;
|
|
416
|
+
|
|
417
|
+
// client.ts
|
|
418
|
+
const client = createRPCClient<AppRouter>({ baseUrl: 'http://localhost:3000/_rpc' });
|
|
419
|
+
const users = await client.users.list.query({ limit: 10 });
|
|
420
|
+
const newUser = await client.users.create.mutate({ name: 'Bob', email: 'bob@example.com' });
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Try the Demo
|
|
424
|
+
|
|
425
|
+
The [SaaS Demo](examples/saas-demo/) builds a complete backend in one file (~250 lines): JWT auth, users CRUD, background tasks, cron, SSE, and OpenAPI docs.
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
cd examples/saas-demo
|
|
429
|
+
npm install
|
|
430
|
+
npx tsx src/index.ts
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Then hit `http://localhost:3000/docs` for the Swagger UI, or:
|
|
434
|
+
|
|
435
|
+
```bash
|
|
436
|
+
# Register
|
|
437
|
+
curl -X POST http://localhost:3000/register \
|
|
438
|
+
-H 'Content-Type: application/json' \
|
|
439
|
+
-d '{"email":"alice@example.com","password":"secret123","name":"Alice"}'
|
|
440
|
+
|
|
441
|
+
# Login and grab the token
|
|
442
|
+
curl -X POST http://localhost:3000/login \
|
|
443
|
+
-H 'Content-Type: application/json' \
|
|
444
|
+
-d '{"email":"alice@example.com","password":"secret123"}'
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Ecosystem
|
|
448
|
+
|
|
449
|
+
### Core Packages
|
|
450
|
+
|
|
451
|
+
| Package | Description |
|
|
452
|
+
| ------- | ----------- |
|
|
453
|
+
| `@celsian/core` | Server runtime, routing, hooks, plugins, task queue, cron, WebSocket, CORS, security, database, OpenAPI |
|
|
454
|
+
| `@celsian/schema` | Standard Schema adapters -- auto-detects Zod, TypeBox, Valibot |
|
|
455
|
+
| `@celsian/rpc` | Type-safe RPC procedures, middleware, OpenAPI generation, typed client |
|
|
456
|
+
| `@celsian/jwt` | JWT sign/verify plugin with route guard helper |
|
|
457
|
+
| `@celsian/cache` | KV store, response caching, session management |
|
|
458
|
+
| `@celsian/rate-limit` | Fixed-window rate limiter with pluggable store |
|
|
459
|
+
| `@celsian/compress` | Response compression (Brotli, gzip, deflate) |
|
|
460
|
+
| `@celsian/queue-redis` | Redis-backed task queue for production |
|
|
461
|
+
|
|
462
|
+
### Deployment Adapters
|
|
463
|
+
|
|
464
|
+
| Package | Target |
|
|
465
|
+
| ------- | ------ |
|
|
466
|
+
| `@celsian/adapter-cloudflare` | Cloudflare Workers (env bindings, execution context) |
|
|
467
|
+
| `@celsian/adapter-lambda` | AWS Lambda + API Gateway v2 |
|
|
468
|
+
| `@celsian/adapter-vercel` | Vercel Serverless + Edge Functions |
|
|
469
|
+
| `@celsian/adapter-node` | Standalone Node.js server |
|
|
470
|
+
| `@celsian/adapter-fly` | Fly.io (generates fly.toml, Dockerfile, multi-region) |
|
|
471
|
+
| `@celsian/adapter-railway` | Railway (generates railway.json, Procfile) |
|
|
472
|
+
|
|
473
|
+
### Tooling
|
|
474
|
+
|
|
475
|
+
| Package | Description |
|
|
476
|
+
| ------- | ----------- |
|
|
477
|
+
| `create-celsian` | Project scaffolder (`npx create-celsian my-api`) |
|
|
478
|
+
| `@celsian/cli` | Dev server, route listing, code generation |
|
|
479
|
+
| `celsian` | Meta-package for single-import convenience |
|
|
480
|
+
|
|
481
|
+
## Production Features
|
|
482
|
+
|
|
483
|
+
### Graceful Shutdown
|
|
484
|
+
|
|
485
|
+
On SIGTERM/SIGINT: stops accepting connections, drains in-flight requests, stops workers and cron, runs cleanup.
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
serve(app, {
|
|
489
|
+
shutdownTimeout: 15_000,
|
|
490
|
+
onShutdown: () => db.close(),
|
|
491
|
+
});
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Health Checks and Route Manifest
|
|
495
|
+
|
|
496
|
+
```typescript
|
|
497
|
+
app.health({ check: () => pool.isHealthy() }); // /health + /ready
|
|
498
|
+
|
|
499
|
+
// Tag routes for deployment tooling
|
|
500
|
+
app.route({ method: 'GET', url: '/api/users', kind: 'serverless', handler });
|
|
501
|
+
app.route({ method: 'GET', url: '/ws', kind: 'hot', handler });
|
|
502
|
+
const manifest = app.getRouteManifest(); // { serverless: [...], hot: [...], task: [...] }
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Database Analytics
|
|
506
|
+
|
|
507
|
+
Wrap your pool with `trackedPool()` for per-request query metrics, `Server-Timing` headers, and slow query logging -- zero handler changes. See [Database Plugin](docs/database.md).
|
|
508
|
+
|
|
509
|
+
```typescript
|
|
510
|
+
const pool = trackedPool(pgPool);
|
|
511
|
+
await app.register(database({ createPool: () => pool }), { encapsulate: false });
|
|
512
|
+
await app.register(dbAnalytics({ slowThreshold: 100 }), { encapsulate: false });
|
|
513
|
+
// Response: Server-Timing: db;dur=12.5;desc="3 queries"
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### Testing Without a Server
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
const response = await app.inject({ method: 'GET', url: '/hello' });
|
|
520
|
+
const body = await response.json(); // { hello: 'world' }
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## Deployment
|
|
524
|
+
|
|
525
|
+
Swap the entry point for the JavaScript runtime you are deploying to. See [Deployment Guide](docs/deployment.md) for full instructions and provider-specific credential/config requirements.
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
serve(app, { port: 3000 }); // Node / Bun / Deno
|
|
529
|
+
|
|
530
|
+
export default createCloudflareHandler(app); // Cloudflare Workers
|
|
531
|
+
export const handler = createLambdaHandler(app); // AWS Lambda
|
|
532
|
+
export default createVercelHandler(app); // Vercel Serverless
|
|
533
|
+
export default createVercelEdgeHandler(app); // Vercel Edge
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
Fly.io and Railway adapters auto-generate deployment configs (`fly.toml`, Dockerfile, `railway.json`). Adapter packages are covered by local generation/package smoke tests; live cloud deployment still depends on each provider's credentials and project configuration.
|
|
537
|
+
|
|
538
|
+
## Benchmark Results
|
|
539
|
+
|
|
540
|
+
Node.js v22.13.1, macOS Darwin (Apple Silicon), 10 connections, 10s per scenario.
|
|
541
|
+
|
|
542
|
+
| Scenario | Fastify (req/s) | CelsianJS (req/s) | Express (req/s) |
|
|
543
|
+
| --------------------- | ---------------: | -----------------: | --------------: |
|
|
544
|
+
| JSON response | 45,866 | 27,996 | 16,321 |
|
|
545
|
+
| Route params | 45,440 | 27,026 | 16,288 |
|
|
546
|
+
| Middleware (5) | 41,380 | 24,445 | 15,751 |
|
|
547
|
+
| JSON body parsing | 29,998 | 19,074 | 14,648 |
|
|
548
|
+
| Error handling | 32,398 | 18,542 | 14,765 |
|
|
549
|
+
|
|
550
|
+
Fastify is the fastest Node.js framework. CelsianJS is 1.3-1.7x faster than Express. The gap with Fastify comes from Web Standard API overhead (`Request`/`Response` per request). CelsianJS trades some throughput for multi-runtime portability and built-in application infrastructure.
|
|
551
|
+
|
|
552
|
+
## Configuration
|
|
553
|
+
|
|
554
|
+
CelsianJS loads `celsian.config.ts` (or `.js`/`.mjs`) automatically:
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
import { defineConfig } from '@celsian/core';
|
|
558
|
+
|
|
559
|
+
export default defineConfig({
|
|
560
|
+
server: { port: 3000, host: 'localhost', trustProxy: true },
|
|
561
|
+
schema: { provider: 'auto' }, // or 'zod' | 'typebox' | 'valibot'
|
|
562
|
+
});
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## Documentation
|
|
566
|
+
|
|
567
|
+
- [Quick Start Guide](docs/quickstart.md)
|
|
568
|
+
- [Hooks Lifecycle](docs/hooks.md)
|
|
569
|
+
- [Plugins and Encapsulation](docs/plugins.md)
|
|
570
|
+
- [Deployment Guide](docs/deployment.md)
|
|
571
|
+
- [Database Plugin](docs/database.md)
|
|
572
|
+
|
|
573
|
+
## WhatStack
|
|
574
|
+
|
|
575
|
+
CelsianJS is the backend half of [WhatStack](https://whatfw.com) — the agent-first full-stack framework:
|
|
576
|
+
|
|
577
|
+
| Layer | Framework | What It Does |
|
|
578
|
+
|-------|-----------|-------------|
|
|
579
|
+
| Frontend | [WhatFW](https://whatfw.com) | Signals, fine-grained rendering, MCP DevTools |
|
|
580
|
+
| Backend | **CelsianJS** | Hooks, plugins, tasks, cron, RPC, multi-runtime |
|
|
581
|
+
| Deploy | [Vura](https://github.com/zvndev/vura) | Platform deployment (coming soon) |
|
|
582
|
+
|
|
583
|
+
## Contributing
|
|
584
|
+
|
|
585
|
+
```bash
|
|
586
|
+
git clone https://github.com/CelsianJs/celsian.git
|
|
587
|
+
cd celsian
|
|
588
|
+
pnpm install
|
|
589
|
+
pnpm test
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
The project uses pnpm workspaces. All packages are in `packages/`. Tests use Vitest.
|
|
593
|
+
|
|
594
|
+
## License
|
|
595
|
+
|
|
596
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celsian/adapter-railway",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "CelsianJS adapter for Railway — generates Procfile, Dockerfile, and health check config",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dist"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@celsian/core": "0.3.
|
|
20
|
+
"@celsian/core": "0.3.9"
|
|
21
21
|
},
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|