@archlast/server 0.0.1 → 0.1.1
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 +938 -11
- package/docker/README.md +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,32 +1,959 @@
|
|
|
1
1
|
# @archlast/server
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
runtime metadata and templates.
|
|
3
|
+
Type-safe server definitions and runtime helpers for Archlast. This package provides schema definition with validators, function definitions (query, mutation, action, http, webhook, rpc), background job scheduling, and shared types. The runtime server is delivered via Docker image.
|
|
5
4
|
|
|
6
|
-
##
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Library vs Runtime](#library-vs-runtime)
|
|
9
|
+
- [Schema Definition](#schema-definition)
|
|
10
|
+
- [Field Types (Validators)](#field-types-validators)
|
|
11
|
+
- [Field Modifiers](#field-modifiers)
|
|
12
|
+
- [Relationships](#relationships)
|
|
13
|
+
- [Indexes](#indexes)
|
|
14
|
+
- [Function Definitions](#function-definitions)
|
|
15
|
+
- [Query](#query)
|
|
16
|
+
- [Mutation](#mutation)
|
|
17
|
+
- [Action](#action)
|
|
18
|
+
- [HTTP Routes](#http-routes)
|
|
19
|
+
- [Webhooks](#webhooks)
|
|
20
|
+
- [RPC (tRPC-style)](#rpc-trpc-style)
|
|
21
|
+
- [Context Objects](#context-objects)
|
|
22
|
+
- [Authentication & Permissions](#authentication--permissions)
|
|
23
|
+
- [Background Jobs](#background-jobs)
|
|
24
|
+
- [Storage Types](#storage-types)
|
|
25
|
+
- [Subpath Exports](#subpath-exports)
|
|
26
|
+
- [Environment Variables](#environment-variables)
|
|
27
|
+
- [Docker Templates](#docker-templates)
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install -D @archlast/server
|
|
33
|
+
|
|
34
|
+
# Or with other package managers
|
|
35
|
+
pnpm add -D @archlast/server
|
|
36
|
+
yarn add -D @archlast/server
|
|
37
|
+
bun add -D @archlast/server
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Library vs Runtime
|
|
41
|
+
|
|
42
|
+
This package is a **dev-time library** for schema and function definitions. The actual server runtime is delivered via Docker and managed by the CLI:
|
|
7
43
|
|
|
8
44
|
```bash
|
|
9
|
-
|
|
45
|
+
# Use the CLI to run the server
|
|
46
|
+
archlast start
|
|
47
|
+
|
|
48
|
+
# The Docker image contains the full runtime
|
|
49
|
+
docker pull archlast/server:latest
|
|
10
50
|
```
|
|
11
51
|
|
|
12
|
-
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Schema Definition
|
|
55
|
+
|
|
56
|
+
Define your data model in `src/schema.ts`:
|
|
13
57
|
|
|
14
58
|
```ts
|
|
15
59
|
import { defineSchema, defineTable, v } from "@archlast/server/schema/definition";
|
|
16
60
|
|
|
17
61
|
export default defineSchema({
|
|
18
62
|
tasks: defineTable({
|
|
19
|
-
|
|
63
|
+
_id: v.id().primaryKey(),
|
|
20
64
|
text: v.string(),
|
|
65
|
+
completed: v.boolean().default(false),
|
|
66
|
+
priority: v.string().default("medium"),
|
|
67
|
+
createdAt: v.number().createdNow(),
|
|
68
|
+
updatedAt: v.number().updateNow().optional(),
|
|
69
|
+
}),
|
|
70
|
+
|
|
71
|
+
users: defineTable({
|
|
72
|
+
_id: v.id().primaryKey(),
|
|
73
|
+
email: v.string().unique(),
|
|
74
|
+
name: v.string(),
|
|
75
|
+
role: v.string().default("user"),
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Field Types (Validators)
|
|
81
|
+
|
|
82
|
+
The `v` object provides all available field types:
|
|
83
|
+
|
|
84
|
+
| Validator | TypeScript Type | Description |
|
|
85
|
+
|-----------|-----------------|-------------|
|
|
86
|
+
| `v.id()` | `string` | CUID2 identifier (auto-generated) |
|
|
87
|
+
| `v.uuid()` | `string` | UUID v4 identifier |
|
|
88
|
+
| `v.objectId()` | `string` | MongoDB-style ObjectId |
|
|
89
|
+
| `v.string()` | `string` | Text field |
|
|
90
|
+
| `v.number()` | `number` | Numeric field (integer or float) |
|
|
91
|
+
| `v.boolean()` | `boolean` | True/false field |
|
|
92
|
+
| `v.date()` | `Date` | Date object |
|
|
93
|
+
| `v.datetime()` | `Date` | Date with time |
|
|
94
|
+
| `v.bytes()` | `Uint8Array` | Binary data |
|
|
95
|
+
| `v.json()` | `unknown` | Arbitrary JSON |
|
|
96
|
+
| `v.any()` | `any` | Any type (use sparingly) |
|
|
97
|
+
| `v.object(shape)` | `object` | Nested object with schema |
|
|
98
|
+
| `v.array(schema)` | `T[]` | Array of items |
|
|
99
|
+
| `v.optional(type)` | `T \| undefined` | Optional wrapper |
|
|
100
|
+
|
|
101
|
+
### Examples
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { v } from "@archlast/server/schema/validators";
|
|
105
|
+
|
|
106
|
+
// Basic types
|
|
107
|
+
v.string() // string
|
|
108
|
+
v.number() // number
|
|
109
|
+
v.boolean() // boolean
|
|
110
|
+
|
|
111
|
+
// IDs
|
|
112
|
+
v.id() // CUID2: "clxxxx..."
|
|
113
|
+
v.uuid() // UUID: "550e8400-e29b-41d4-a716-446655440000"
|
|
114
|
+
v.objectId() // ObjectId: "507f1f77bcf86cd799439011"
|
|
115
|
+
|
|
116
|
+
// Dates
|
|
117
|
+
v.date() // Date object (date only)
|
|
118
|
+
v.datetime() // Date object (with time)
|
|
119
|
+
|
|
120
|
+
// Complex types
|
|
121
|
+
v.json() // Any JSON-serializable value
|
|
122
|
+
v.bytes() // Binary data as Uint8Array
|
|
123
|
+
|
|
124
|
+
// Nested object
|
|
125
|
+
v.object({
|
|
126
|
+
street: v.string(),
|
|
127
|
+
city: v.string(),
|
|
128
|
+
zip: v.string(),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Array
|
|
132
|
+
v.array(v.string()) // string[]
|
|
133
|
+
v.array(v.object({ // Array of objects
|
|
134
|
+
name: v.string(),
|
|
135
|
+
score: v.number(),
|
|
136
|
+
}))
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Field Modifiers
|
|
140
|
+
|
|
141
|
+
Chain modifiers to add constraints and behaviors:
|
|
142
|
+
|
|
143
|
+
| Modifier | Description |
|
|
144
|
+
|----------|-------------|
|
|
145
|
+
| `.primaryKey()` | Mark as primary key (auto-generates if not provided) |
|
|
146
|
+
| `.unique()` | Unique constraint |
|
|
147
|
+
| `.index()` | Create index for faster queries |
|
|
148
|
+
| `.searchable()` | Enable full-text search |
|
|
149
|
+
| `.optional()` | Make field nullable (TypeScript: `T \| undefined`) |
|
|
150
|
+
| `.default(value)` | Default value on insert |
|
|
151
|
+
| `.createdNow()` | Auto-set to `Date.now()` on insert |
|
|
152
|
+
| `.updateNow()` | Auto-set to `Date.now()` on every update |
|
|
153
|
+
| `.foreignKey(table, field, options)` | Foreign key reference |
|
|
154
|
+
|
|
155
|
+
### Examples
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
defineTable({
|
|
159
|
+
// Primary key with auto-generation
|
|
160
|
+
_id: v.id().primaryKey(),
|
|
161
|
+
|
|
162
|
+
// Unique constraint
|
|
163
|
+
email: v.string().unique(),
|
|
164
|
+
|
|
165
|
+
// Indexed for fast lookups
|
|
166
|
+
slug: v.string().index(),
|
|
167
|
+
|
|
168
|
+
// Full-text searchable
|
|
169
|
+
content: v.string().searchable(),
|
|
170
|
+
|
|
171
|
+
// Optional field
|
|
172
|
+
bio: v.string().optional(),
|
|
173
|
+
|
|
174
|
+
// Default values
|
|
175
|
+
role: v.string().default("user"),
|
|
176
|
+
count: v.number().default(0),
|
|
177
|
+
active: v.boolean().default(true),
|
|
178
|
+
|
|
179
|
+
// Automatic timestamps
|
|
180
|
+
createdAt: v.number().createdNow(),
|
|
181
|
+
updatedAt: v.number().updateNow().optional(),
|
|
182
|
+
|
|
183
|
+
// Foreign key
|
|
184
|
+
authorId: v.id().foreignKey("users", "_id", {
|
|
185
|
+
onDelete: "cascade",
|
|
186
|
+
onUpdate: "cascade",
|
|
187
|
+
}),
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Relationships
|
|
192
|
+
|
|
193
|
+
Define relationships in the second argument of `defineTable`:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import {
|
|
197
|
+
defineTable,
|
|
198
|
+
hasOne,
|
|
199
|
+
hasMany,
|
|
200
|
+
belongsTo,
|
|
201
|
+
manyToMany
|
|
202
|
+
} from "@archlast/server/schema/definition";
|
|
203
|
+
|
|
204
|
+
export default defineSchema({
|
|
205
|
+
users: defineTable(
|
|
206
|
+
{
|
|
207
|
+
_id: v.id().primaryKey(),
|
|
208
|
+
name: v.string(),
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
relationships: {
|
|
212
|
+
// User has many tasks
|
|
213
|
+
tasks: hasMany("tasks", "authorId"),
|
|
214
|
+
|
|
215
|
+
// User has one profile
|
|
216
|
+
profile: hasOne("profiles", "userId"),
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
),
|
|
220
|
+
|
|
221
|
+
tasks: defineTable(
|
|
222
|
+
{
|
|
223
|
+
_id: v.id().primaryKey(),
|
|
224
|
+
text: v.string(),
|
|
225
|
+
authorId: v.id(),
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
relationships: {
|
|
229
|
+
// Task belongs to user
|
|
230
|
+
author: belongsTo("users", "authorId"),
|
|
231
|
+
|
|
232
|
+
// Task has many subtasks
|
|
233
|
+
subtasks: hasMany("subtasks", "taskId"),
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
),
|
|
237
|
+
|
|
238
|
+
tags: defineTable(
|
|
239
|
+
{
|
|
240
|
+
_id: v.id().primaryKey(),
|
|
241
|
+
name: v.string(),
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
relationships: {
|
|
245
|
+
// Many-to-many through junction table
|
|
246
|
+
tasks: manyToMany("tasks", "task_tags", "tagId", "taskId"),
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
),
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Indexes
|
|
254
|
+
|
|
255
|
+
Create composite indexes for complex queries:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
defineTable(
|
|
259
|
+
{
|
|
260
|
+
_id: v.id().primaryKey(),
|
|
261
|
+
completed: v.boolean(),
|
|
262
|
+
priority: v.string(),
|
|
263
|
+
createdAt: v.number(),
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
indexes: [
|
|
267
|
+
// Composite index
|
|
268
|
+
{ fields: ["completed", "createdAt"], name: "idx_tasks_status" },
|
|
269
|
+
|
|
270
|
+
// Single field index
|
|
271
|
+
{ fields: ["priority"], name: "idx_tasks_priority" },
|
|
272
|
+
],
|
|
273
|
+
}
|
|
274
|
+
)
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Function Definitions
|
|
280
|
+
|
|
281
|
+
Functions are the API of your Archlast application. Import from `@archlast/server/functions/definition`.
|
|
282
|
+
|
|
283
|
+
### Query
|
|
284
|
+
|
|
285
|
+
Read-only operations. Results are cached and reactive.
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import { query } from "@archlast/server/functions/definition";
|
|
289
|
+
|
|
290
|
+
// Simple query
|
|
291
|
+
export const list = query(async (ctx) => {
|
|
292
|
+
return ctx.db.query("tasks").findMany();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Query with arguments
|
|
296
|
+
export const get = query({
|
|
297
|
+
args: { id: v.string().zodSchema },
|
|
298
|
+
handler: async (ctx, args) => {
|
|
299
|
+
return ctx.db.get("tasks", args.id);
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Query with filtering
|
|
304
|
+
export const listByStatus = query({
|
|
305
|
+
args: {
|
|
306
|
+
completed: v.boolean().zodSchema,
|
|
307
|
+
limit: v.number().optional().zodSchema,
|
|
308
|
+
},
|
|
309
|
+
handler: async (ctx, args) => {
|
|
310
|
+
return ctx.db
|
|
311
|
+
.query("tasks")
|
|
312
|
+
.where({ completed: args.completed })
|
|
313
|
+
.take(args.limit ?? 10)
|
|
314
|
+
.findMany();
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Mutation
|
|
320
|
+
|
|
321
|
+
Write operations. Automatically invalidate related caches.
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
import { mutation } from "@archlast/server/functions/definition";
|
|
325
|
+
|
|
326
|
+
export const create = mutation({
|
|
327
|
+
args: {
|
|
328
|
+
text: v.string().zodSchema,
|
|
329
|
+
priority: v.string().optional().zodSchema,
|
|
330
|
+
},
|
|
331
|
+
handler: async (ctx, args) => {
|
|
332
|
+
return ctx.db.insert("tasks", {
|
|
333
|
+
text: args.text,
|
|
334
|
+
priority: args.priority ?? "medium",
|
|
335
|
+
completed: false,
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
export const update = mutation({
|
|
341
|
+
args: {
|
|
342
|
+
id: v.string().zodSchema,
|
|
343
|
+
text: v.string().optional().zodSchema,
|
|
344
|
+
completed: v.boolean().optional().zodSchema,
|
|
345
|
+
},
|
|
346
|
+
handler: async (ctx, args) => {
|
|
347
|
+
const { id, ...data } = args;
|
|
348
|
+
return ctx.db.update("tasks", id, data);
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
export const remove = mutation({
|
|
353
|
+
args: { id: v.string().zodSchema },
|
|
354
|
+
handler: async (ctx, args) => {
|
|
355
|
+
await ctx.db.delete("tasks", args.id);
|
|
356
|
+
return { success: true };
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### Action
|
|
362
|
+
|
|
363
|
+
Long-running operations, side effects, external API calls.
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import { action } from "@archlast/server/functions/definition";
|
|
367
|
+
|
|
368
|
+
export const sendEmail = action({
|
|
369
|
+
args: {
|
|
370
|
+
to: v.string().zodSchema,
|
|
371
|
+
subject: v.string().zodSchema,
|
|
372
|
+
body: v.string().zodSchema,
|
|
373
|
+
},
|
|
374
|
+
handler: async (ctx, args) => {
|
|
375
|
+
// Call external email service
|
|
376
|
+
const response = await fetch("https://api.sendgrid.com/v3/mail/send", {
|
|
377
|
+
method: "POST",
|
|
378
|
+
headers: {
|
|
379
|
+
"Authorization": `Bearer ${process.env.SENDGRID_API_KEY}`,
|
|
380
|
+
"Content-Type": "application/json",
|
|
381
|
+
},
|
|
382
|
+
body: JSON.stringify({
|
|
383
|
+
personalizations: [{ to: [{ email: args.to }] }],
|
|
384
|
+
from: { email: "noreply@myapp.com" },
|
|
385
|
+
subject: args.subject,
|
|
386
|
+
content: [{ type: "text/plain", value: args.body }],
|
|
387
|
+
}),
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
return { sent: response.ok };
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
export const processImage = action({
|
|
395
|
+
args: { storageId: v.string().zodSchema },
|
|
396
|
+
handler: async (ctx, args) => {
|
|
397
|
+
const file = await ctx.storage.get(args.storageId);
|
|
398
|
+
// Process image...
|
|
399
|
+
const processedId = await ctx.storage.store("processed", processedBuffer);
|
|
400
|
+
return { processedId };
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
### HTTP Routes
|
|
406
|
+
|
|
407
|
+
Explicit HTTP endpoints with full control over request/response.
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
import { http } from "@archlast/server/http/definition";
|
|
411
|
+
|
|
412
|
+
// GET route
|
|
413
|
+
export const health = http.get({
|
|
414
|
+
path: "/health",
|
|
415
|
+
handler: async (ctx) => {
|
|
416
|
+
return new Response("ok", { status: 200 });
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// POST route with body parsing
|
|
421
|
+
export const createWebhook = http.post({
|
|
422
|
+
path: "/api/webhooks",
|
|
423
|
+
handler: async (ctx) => {
|
|
424
|
+
const body = await ctx.req.json();
|
|
425
|
+
// Process webhook...
|
|
426
|
+
return Response.json({ received: true });
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// Route with path parameters
|
|
431
|
+
export const getUser = http.get({
|
|
432
|
+
path: "/api/users/:id",
|
|
433
|
+
handler: async (ctx) => {
|
|
434
|
+
const userId = ctx.req.params.id;
|
|
435
|
+
const user = await ctx.server.db.get("users", userId);
|
|
436
|
+
if (!user) {
|
|
437
|
+
return new Response("Not found", { status: 404 });
|
|
438
|
+
}
|
|
439
|
+
return Response.json(user);
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// Other methods
|
|
444
|
+
export const updateResource = http.put({
|
|
445
|
+
path: "/api/resource/:id",
|
|
446
|
+
handler: async (ctx) => {
|
|
447
|
+
const id = ctx.req.params.id;
|
|
448
|
+
const data = await ctx.req.json();
|
|
449
|
+
// Update resource...
|
|
450
|
+
return Response.json({ success: true });
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
export const patchResource = http.patch({
|
|
455
|
+
path: "/api/resource/:id",
|
|
456
|
+
handler: async (ctx) => { /* ... */ return Response.json({ success: true }); },
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
export const deleteResource = http.delete({
|
|
460
|
+
path: "/api/resource/:id",
|
|
461
|
+
handler: async (ctx) => { /* ... */ return new Response(null, { status: 204 }); },
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
export const optionsResource = http.options({
|
|
465
|
+
path: "/api/resource",
|
|
466
|
+
handler: async (ctx) => { return new Response(null, { status: 204 }); },
|
|
467
|
+
});
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Webhooks
|
|
471
|
+
|
|
472
|
+
Webhook handlers with custom security guards.
|
|
473
|
+
|
|
474
|
+
```ts
|
|
475
|
+
import { webhook } from "@archlast/server/webhook/definition";
|
|
476
|
+
import { createSignatureGuard } from "@archlast/server/webhook/guard";
|
|
477
|
+
|
|
478
|
+
// Stripe webhook with signature verification
|
|
479
|
+
export const stripeWebhook = webhook({
|
|
480
|
+
path: "/webhooks/stripe",
|
|
481
|
+
method: "POST",
|
|
482
|
+
guards: [
|
|
483
|
+
createSignatureGuard({
|
|
484
|
+
secret: process.env.STRIPE_WEBHOOK_SECRET!,
|
|
485
|
+
header: "stripe-signature",
|
|
486
|
+
algorithm: "sha256",
|
|
487
|
+
}),
|
|
488
|
+
],
|
|
489
|
+
handler: async (ctx, event) => {
|
|
490
|
+
switch (event.type) {
|
|
491
|
+
case "checkout.session.completed":
|
|
492
|
+
await ctx.db.insert("orders", { userId: event.data.customer });
|
|
493
|
+
break;
|
|
494
|
+
case "customer.subscription.deleted":
|
|
495
|
+
await ctx.db.update("users", event.data.customer, { subscribed: false });
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
return { received: true };
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
// GitHub webhook with signature verification
|
|
503
|
+
export const githubWebhook = webhook({
|
|
504
|
+
path: "/webhooks/github",
|
|
505
|
+
guards: [
|
|
506
|
+
createSignatureGuard({
|
|
507
|
+
secret: process.env.GITHUB_WEBHOOK_SECRET!,
|
|
508
|
+
header: "X-Hub-Signature-256",
|
|
509
|
+
algorithm: "sha256",
|
|
510
|
+
}),
|
|
511
|
+
],
|
|
512
|
+
handler: async (ctx, payload) => {
|
|
513
|
+
ctx.logger.info("GitHub event received", { action: payload.action });
|
|
514
|
+
return { ok: true };
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### RPC (tRPC-style)
|
|
520
|
+
|
|
521
|
+
Type-safe RPC procedures with automatic router generation.
|
|
522
|
+
|
|
523
|
+
```ts
|
|
524
|
+
import { rpc } from "@archlast/server/functions/definition";
|
|
525
|
+
import { z } from "zod";
|
|
526
|
+
|
|
527
|
+
// RPC query
|
|
528
|
+
export const getTasks = rpc.query({
|
|
529
|
+
args: z.object({
|
|
530
|
+
completed: z.boolean().optional(),
|
|
21
531
|
}),
|
|
532
|
+
handler: async (ctx, args) => {
|
|
533
|
+
const query = ctx.db.query("tasks");
|
|
534
|
+
if (args.completed !== undefined) {
|
|
535
|
+
query.where({ completed: args.completed });
|
|
536
|
+
}
|
|
537
|
+
return query.findMany();
|
|
538
|
+
},
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// RPC mutation
|
|
542
|
+
export const createTask = rpc.mutation({
|
|
543
|
+
args: z.object({
|
|
544
|
+
text: z.string(),
|
|
545
|
+
}),
|
|
546
|
+
handler: async (ctx, args) => {
|
|
547
|
+
return ctx.db.insert("tasks", { text: args.text, completed: false });
|
|
548
|
+
},
|
|
549
|
+
});
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Context Objects
|
|
555
|
+
|
|
556
|
+
Each function receives a context object with available services.
|
|
557
|
+
|
|
558
|
+
### QueryCtx (for queries)
|
|
559
|
+
|
|
560
|
+
```ts
|
|
561
|
+
interface QueryCtx {
|
|
562
|
+
db: GenericDatabaseReader<DataModel>;
|
|
563
|
+
auth: AuthContext;
|
|
564
|
+
repository: RepositoryMap<DataModel>;
|
|
565
|
+
logger: LogService;
|
|
566
|
+
}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### MutationCtx (for mutations)
|
|
570
|
+
|
|
571
|
+
```ts
|
|
572
|
+
interface MutationCtx extends QueryCtx {
|
|
573
|
+
db: GenericDatabaseWriter<DataModel>;
|
|
574
|
+
scheduler: JobScheduler;
|
|
575
|
+
}
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### ActionCtx (for actions)
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
interface ActionCtx extends MutationCtx {
|
|
582
|
+
storage: StorageClient;
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### AuthContext
|
|
587
|
+
|
|
588
|
+
The auth context is populated from Better-Auth session data:
|
|
589
|
+
|
|
590
|
+
```ts
|
|
591
|
+
interface AuthContext {
|
|
592
|
+
// User ID from Better-Auth session
|
|
593
|
+
userId: string | null;
|
|
594
|
+
|
|
595
|
+
// Session ID from Better-Auth
|
|
596
|
+
sessionId: string | null;
|
|
597
|
+
|
|
598
|
+
// Whether the request is authenticated
|
|
599
|
+
isAuthenticated: boolean;
|
|
600
|
+
|
|
601
|
+
// Better-Auth user object
|
|
602
|
+
user: {
|
|
603
|
+
id: string;
|
|
604
|
+
email: string;
|
|
605
|
+
emailVerified: boolean;
|
|
606
|
+
name?: string;
|
|
607
|
+
image?: string;
|
|
608
|
+
role?: string; // "user", "admin", "super-admin"
|
|
609
|
+
banned?: boolean;
|
|
610
|
+
createdAt: Date;
|
|
611
|
+
updatedAt: Date;
|
|
612
|
+
} | null;
|
|
613
|
+
|
|
614
|
+
// Tenant/Organization info (if using organizations)
|
|
615
|
+
tenant?: {
|
|
616
|
+
id: string;
|
|
617
|
+
name: string;
|
|
618
|
+
ownerId: string;
|
|
619
|
+
} | null;
|
|
620
|
+
|
|
621
|
+
// Membership info
|
|
622
|
+
membership?: {
|
|
623
|
+
role: string;
|
|
624
|
+
permissions: string[];
|
|
625
|
+
} | null;
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**Note:** Better-Auth stores users in `system_auth_user` collection with the `id` field (not `_id`). The adapter handles mapping automatically.
|
|
630
|
+
|
|
631
|
+
### Usage Examples
|
|
632
|
+
|
|
633
|
+
```ts
|
|
634
|
+
export const myQuery = query(async (ctx) => {
|
|
635
|
+
// Database access
|
|
636
|
+
const tasks = await ctx.db.query("tasks").findMany();
|
|
637
|
+
|
|
638
|
+
// LINQ-style repository
|
|
639
|
+
const data = await ctx.repository.tasks
|
|
640
|
+
.Where({ completed: false })
|
|
641
|
+
.OrderBy({ createdAt: "desc" })
|
|
642
|
+
.Take(10)
|
|
643
|
+
.ToArrayAsync();
|
|
644
|
+
|
|
645
|
+
// Auth info
|
|
646
|
+
if (ctx.auth.isAuthenticated) {
|
|
647
|
+
console.log("User:", ctx.auth.userId);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Logging
|
|
651
|
+
ctx.logger.info("Query executed", { count: tasks.length });
|
|
652
|
+
|
|
653
|
+
return tasks;
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
export const myMutation = mutation(async (ctx) => {
|
|
657
|
+
// Schedule background job
|
|
658
|
+
await ctx.scheduler.schedule({
|
|
659
|
+
name: "sendNotification",
|
|
660
|
+
preset: SchedulePreset.RunNow,
|
|
661
|
+
payload: { userId: ctx.auth.userId },
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
export const myAction = action(async (ctx) => {
|
|
666
|
+
// File storage
|
|
667
|
+
const file = await ctx.storage.get("file_id");
|
|
668
|
+
const newId = await ctx.storage.store("bucket", buffer);
|
|
669
|
+
});
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
## Authentication & Permissions
|
|
675
|
+
|
|
676
|
+
Archlast uses [Better-Auth](https://www.better-auth.com/) for authentication. The server mounts Better-Auth at `/api/auth/*` and includes these plugins:
|
|
677
|
+
|
|
678
|
+
- **Email/Password**: Standard credential authentication via `signIn.email()`
|
|
679
|
+
- **Username**: Username-based sign-in via `signIn.username()`
|
|
680
|
+
- **Anonymous**: Guest user support via `signIn.anonymous()`
|
|
681
|
+
- **Admin**: User management, roles, bans, impersonation
|
|
682
|
+
- **API Key**: Programmatic access with `arch_` prefixed keys
|
|
683
|
+
- **Organization**: Multi-tenancy support
|
|
684
|
+
|
|
685
|
+
### Sign-In Methods
|
|
686
|
+
|
|
687
|
+
```ts
|
|
688
|
+
// Email-based sign-in (web app pattern)
|
|
689
|
+
await authClient.signIn.email({ email: "user@example.com", password: "..." });
|
|
690
|
+
|
|
691
|
+
// Username-based sign-in (dashboard pattern)
|
|
692
|
+
await authClient.signIn.username({ username: "admin", password: "..." });
|
|
693
|
+
|
|
694
|
+
// Anonymous/guest sign-in
|
|
695
|
+
await authClient.signIn.anonymous();
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### Better-Auth Configuration
|
|
699
|
+
|
|
700
|
+
Better-Auth is configured in the server with these features:
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
// Environment variables for Better-Auth
|
|
704
|
+
BETTER_AUTH_SECRET=your-32-char-secret // Required in production
|
|
705
|
+
APP_URL=https://your-app.com // Base URL for redirects
|
|
706
|
+
ARCHLAST_ALLOWED_ORIGINS=http://localhost:3000 // Trusted origins (CSV)
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### Auth Modes
|
|
710
|
+
|
|
711
|
+
All functions default to requiring authentication. Override with `auth` option:
|
|
712
|
+
|
|
713
|
+
```ts
|
|
714
|
+
// Required (default) - must be authenticated via Better-Auth
|
|
715
|
+
export const privateQuery = query({
|
|
716
|
+
handler: async (ctx) => {
|
|
717
|
+
// ctx.auth.userId is guaranteed to exist
|
|
718
|
+
// ctx.auth.user contains Better-Auth user object
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
// Optional - authentication checked but not required
|
|
723
|
+
export const optionalAuth = query({
|
|
724
|
+
auth: "optional",
|
|
725
|
+
handler: async (ctx) => {
|
|
726
|
+
if (ctx.auth.isAuthenticated) {
|
|
727
|
+
// Authenticated user via Better-Auth
|
|
728
|
+
} else {
|
|
729
|
+
// Anonymous/guest user
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
// Public - no authentication required
|
|
735
|
+
export const publicQuery = query({
|
|
736
|
+
auth: "public",
|
|
737
|
+
handler: async (ctx) => {
|
|
738
|
+
// Anyone can access
|
|
739
|
+
},
|
|
740
|
+
});
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### Permissions
|
|
744
|
+
|
|
745
|
+
Add permission checks for role-based access:
|
|
746
|
+
|
|
747
|
+
```ts
|
|
748
|
+
export const adminOnly = query({
|
|
749
|
+
permissions: ["admin"],
|
|
750
|
+
handler: async (ctx) => {
|
|
751
|
+
// Only users with "admin" permission
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
export const moderatorOrAdmin = mutation({
|
|
756
|
+
permissions: ["admin", "moderator"],
|
|
757
|
+
handler: async (ctx) => {
|
|
758
|
+
// Users with either permission
|
|
759
|
+
},
|
|
22
760
|
});
|
|
23
761
|
```
|
|
24
762
|
|
|
25
|
-
|
|
763
|
+
---
|
|
764
|
+
|
|
765
|
+
## Background Jobs
|
|
766
|
+
|
|
767
|
+
Schedule background tasks using the job scheduler.
|
|
768
|
+
|
|
769
|
+
```ts
|
|
770
|
+
import { SchedulePreset } from "@archlast/server/jobs";
|
|
771
|
+
|
|
772
|
+
export const scheduleCleanup = mutation(async (ctx) => {
|
|
773
|
+
// Run immediately
|
|
774
|
+
await ctx.scheduler.schedule({
|
|
775
|
+
name: "cleanupOldRecords",
|
|
776
|
+
preset: SchedulePreset.RunNow,
|
|
777
|
+
payload: { olderThanDays: 30 },
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// Run every 5 minutes
|
|
781
|
+
await ctx.scheduler.schedule({
|
|
782
|
+
name: "sendReminder",
|
|
783
|
+
preset: SchedulePreset.Every5Minutes,
|
|
784
|
+
payload: { userId: ctx.auth.userId },
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// Run every 10 minutes
|
|
788
|
+
await ctx.scheduler.schedule({
|
|
789
|
+
name: "syncData",
|
|
790
|
+
preset: SchedulePreset.Every10Minutes,
|
|
791
|
+
payload: {},
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
// Recurring with cron
|
|
795
|
+
await ctx.scheduler.schedule({
|
|
796
|
+
name: "dailyReport",
|
|
797
|
+
cron: "0 9 * * *", // Every day at 9 AM
|
|
798
|
+
payload: {},
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
### Schedule Presets
|
|
804
|
+
|
|
805
|
+
| Preset | Cron Expression | Description |
|
|
806
|
+
|--------|-----------------|-------------|
|
|
807
|
+
| `RunNow` | (immediate) | Execute immediately |
|
|
808
|
+
| `EveryMinute` | `* * * * *` | Run every minute |
|
|
809
|
+
| `Every5Minutes` | `*/5 * * * *` | Run every 5 minutes |
|
|
810
|
+
| `Every10Minutes` | `*/10 * * * *` | Run every 10 minutes |
|
|
811
|
+
| `Every30Minutes` | `*/30 * * * *` | Run every 30 minutes |
|
|
812
|
+
| `Hourly` | `0 * * * *` | Run every hour |
|
|
813
|
+
| `Every8Hours` | `0 */8 * * *` | Run every 8 hours |
|
|
814
|
+
| `Every16Hours` | `0 */16 * * *` | Run every 16 hours |
|
|
815
|
+
| `DailyMidnight` | `0 0 * * *` | Daily at midnight UTC |
|
|
816
|
+
| `Weekly` | `0 0 * * 0` | Weekly on Sunday midnight |
|
|
817
|
+
| `Monthly` | `0 0 1 * *` | Monthly on the 1st |
|
|
818
|
+
| `Quarterly` | `0 0 1 */3 *` | Quarterly (every 3 months) |
|
|
819
|
+
|
|
820
|
+
You can also use custom cron expressions:
|
|
821
|
+
|
|
822
|
+
```ts
|
|
823
|
+
await ctx.scheduler.schedule({
|
|
824
|
+
name: "customJob",
|
|
825
|
+
cron: "0 9 * * 1-5", // Weekdays at 9 AM
|
|
826
|
+
payload: {},
|
|
827
|
+
});
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
---
|
|
831
|
+
|
|
832
|
+
## Storage Types
|
|
833
|
+
|
|
834
|
+
File storage type definitions.
|
|
835
|
+
|
|
836
|
+
```ts
|
|
837
|
+
import type { StorageFile, StorageMetadata } from "@archlast/server/storage/types";
|
|
838
|
+
|
|
839
|
+
interface StorageFile {
|
|
840
|
+
id: string;
|
|
841
|
+
bucket: string;
|
|
842
|
+
filename: string;
|
|
843
|
+
contentType: string;
|
|
844
|
+
size: number;
|
|
845
|
+
createdAt: Date;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
interface StorageMetadata {
|
|
849
|
+
filename: string;
|
|
850
|
+
contentType: string;
|
|
851
|
+
size: number;
|
|
852
|
+
bucket: string;
|
|
853
|
+
createdAt: Date;
|
|
854
|
+
url?: string;
|
|
855
|
+
}
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
860
|
+
## Subpath Exports
|
|
861
|
+
|
|
862
|
+
Import only what you need:
|
|
863
|
+
|
|
864
|
+
| Import | Description |
|
|
865
|
+
|--------|-------------|
|
|
866
|
+
| `@archlast/server/schema/definition` | `defineSchema`, `defineTable`, relationship helpers |
|
|
867
|
+
| `@archlast/server/schema/validators` | `v` validator object |
|
|
868
|
+
| `@archlast/server/functions/definition` | `query`, `mutation`, `action`, `rpc` |
|
|
869
|
+
| `@archlast/server/functions/types` | Context types, function types |
|
|
870
|
+
| `@archlast/server/http/definition` | `http` route builder |
|
|
871
|
+
| `@archlast/server/http/router` | HTTP router utilities |
|
|
872
|
+
| `@archlast/server/http` | HTTP utilities |
|
|
873
|
+
| `@archlast/server/webhook/definition` | `webhook` function builder |
|
|
874
|
+
| `@archlast/server/webhook/guard` | Webhook guard utilities |
|
|
875
|
+
| `@archlast/server/webhook/verifier` | Signature verification |
|
|
876
|
+
| `@archlast/server/webhook` | Webhook utilities |
|
|
877
|
+
| `@archlast/server/jobs` | `SchedulePreset`, `Scheduler`, `JobQueue` |
|
|
878
|
+
| `@archlast/server/jobs/scheduler` | Scheduler class and presets |
|
|
879
|
+
| `@archlast/server/jobs/queue` | Job queue implementation |
|
|
880
|
+
| `@archlast/server/storage/types` | Storage type definitions |
|
|
881
|
+
| `@archlast/server/context` | Server context exports |
|
|
882
|
+
| `@archlast/server/db/interfaces` | Database interface types |
|
|
883
|
+
| `@archlast/server/auth/interfaces` | Auth interface types |
|
|
884
|
+
| `@archlast/server/repository/interfaces` | Repository interface types |
|
|
885
|
+
| `@archlast/server/repository/factory` | Repository factory |
|
|
886
|
+
| `@archlast/server/di/decorators` | DI decorators |
|
|
887
|
+
| `@archlast/server/di/container` | DI container |
|
|
888
|
+
| `@archlast/server/logging/logger` | Logger service |
|
|
889
|
+
| `@archlast/server/docker` | Docker utilities |
|
|
890
|
+
|
|
891
|
+
---
|
|
892
|
+
|
|
893
|
+
## Environment Variables
|
|
894
|
+
|
|
895
|
+
Key variables used by the server runtime:
|
|
896
|
+
|
|
897
|
+
### Better-Auth Configuration
|
|
898
|
+
|
|
899
|
+
| Variable | Default | Description |
|
|
900
|
+
|----------|---------|-------------|
|
|
901
|
+
| `BETTER_AUTH_SECRET` | - | **Required in production.** Secret for signing tokens |
|
|
902
|
+
| `APP_URL` | `http://localhost:4000` | Base URL for auth redirects |
|
|
903
|
+
| `BETTER_AUTH_DEBUG` | `false` | Enable debug logging |
|
|
904
|
+
|
|
905
|
+
### Server Configuration
|
|
906
|
+
|
|
907
|
+
| Variable | Default | Description |
|
|
908
|
+
|----------|---------|-------------|
|
|
909
|
+
| `PORT` | `4000` | HTTP server port |
|
|
910
|
+
| `NODE_ENV` | `development` | Environment mode |
|
|
911
|
+
| `ARCHLAST_DB_ROOT` | `./data` | Database file directory |
|
|
912
|
+
| `ARCHLAST_ALLOWED_ORIGINS` | - | CORS allowed origins (CSV) |
|
|
913
|
+
| `ARCHLAST_CORS_ALLOW_CREDENTIALS` | `false` | Allow credentials in CORS |
|
|
914
|
+
|
|
915
|
+
### Storage Configuration
|
|
916
|
+
|
|
917
|
+
| Variable | Default | Description |
|
|
918
|
+
|----------|---------|-------------|
|
|
919
|
+
| `STORAGE_ROOT` | `./storage` | Local file storage directory |
|
|
920
|
+
| `STORAGE_SIGNING_SECRET` | - | Secret for signed URLs |
|
|
921
|
+
| `S3_ENABLED` | `false` | Enable S3 storage |
|
|
922
|
+
| `S3_BUCKET` | - | S3 bucket name |
|
|
923
|
+
| `S3_REGION` | `us-east-1` | S3 region |
|
|
924
|
+
| `AWS_ACCESS_KEY_ID` | - | AWS access key |
|
|
925
|
+
| `AWS_SECRET_ACCESS_KEY` | - | AWS secret key |
|
|
926
|
+
|
|
927
|
+
### Dashboard & Store Configuration
|
|
928
|
+
|
|
929
|
+
| Variable | Default | Description |
|
|
930
|
+
|----------|---------|-------------|
|
|
931
|
+
| `ARCHLAST_DASHBOARD_DIR` | - | Dashboard static files path |
|
|
932
|
+
| `ARCHLAST_DASHBOARD_URL` | - | Dashboard proxy URL |
|
|
933
|
+
| `ARCHLAST_STORE_PORT` | `7001` | Document store port |
|
|
934
|
+
| `ARCHLAST_STORE_HOST` | `127.0.0.1` | Document store host |
|
|
935
|
+
| `ARCHLAST_STORE_NO_TLS` | `false` | Disable TLS for document store |
|
|
936
|
+
|
|
937
|
+
---
|
|
938
|
+
|
|
939
|
+
## Docker Templates
|
|
940
|
+
|
|
941
|
+
Templates for Docker deployment are included in `templates/`:
|
|
942
|
+
|
|
943
|
+
- `docker-compose.yml` - Base compose file
|
|
944
|
+
- `docker-compose.dev.yml` - Development overrides
|
|
945
|
+
- `docker-compose.prod.yml` - Production configuration
|
|
946
|
+
- `.env.example` - Example environment variables
|
|
947
|
+
- `archlast.config.js` - CLI configuration template
|
|
948
|
+
|
|
949
|
+
The CLI uses these templates when running `archlast start`.
|
|
950
|
+
|
|
951
|
+
---
|
|
952
|
+
|
|
953
|
+
## Keywords
|
|
26
954
|
|
|
27
|
-
|
|
28
|
-
`templates/` in this package.
|
|
955
|
+
archlast, server, backend, reactive, real-time, websocket, typescript, api, baas
|
|
29
956
|
|
|
30
|
-
##
|
|
957
|
+
## License
|
|
31
958
|
|
|
32
|
-
|
|
959
|
+
MIT
|
package/docker/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Archlast Docker Assets
|
|
2
2
|
|
|
3
3
|
This folder is reserved for Docker build assets distributed with the
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
`@archlast/server` package. The production Dockerfile and s6 service
|
|
5
|
+
definitions live in the repository root under `docker/`.
|
|
6
|
+
|
|
7
|
+
If you are packaging a runtime image, use the root `docker/Dockerfile` and the
|
|
8
|
+
service definitions in `docker/s6-rc.d/*`.
|
|
9
|
+
|
|
10
|
+
For local runtime via the CLI, use the templates bundled in
|
|
11
|
+
`packages/server/templates/`.
|