okengine 0.8.0 → 0.9.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/package.json +3 -2
- package/site/content/docs/deployment/docker-swarm.mdx +228 -0
- package/site/content/docs/deployment/docker.mdx +212 -0
- package/site/content/docs/deployment/index.mdx +83 -0
- package/site/content/docs/deployment/kubernetes.mdx +176 -0
- package/site/content/docs/deployment/meta.json +5 -0
- package/site/content/docs/deployment/reverse-proxy.mdx +216 -0
- package/site/content/docs/elements/channel.mdx +3 -1
- package/site/content/docs/elements/signal.mdx +10 -8
- package/site/content/docs/elements/store.mdx +34 -0
- package/site/content/docs/get-started/index.mdx +5 -0
- package/site/content/docs/index.mdx +5 -0
- package/site/content/docs/meta.json +10 -1
- package/site/content/docs/plugins/index.mdx +1 -2
- package/site/content/docs/plugins/magic-link.mdx +2 -2
- package/site/content/docs/plugins/meta.json +1 -2
- package/site/content/docs/plugins/otp.mdx +202 -0
- package/site/content/docs/plugins/two-factor.mdx +2 -2
- package/site/content/docs/reference/cli.md +3 -2
- package/site/content/docs/reference/configuration.mdx +16 -0
- package/site/content/docs/reference/environment-variables.mdx +9 -8
- package/site/content/docs/reference/plugins.mdx +1 -1
- package/src/auth/auth.test.ts +36 -0
- package/src/auth/bindings.ts +3 -12
- package/src/auth/identity.ts +33 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/otp-capability.ts +119 -0
- package/src/auth/otp-seal.test.ts +61 -0
- package/src/auth/otp-seal.ts +84 -0
- package/src/auth/schema.ts +3 -0
- package/src/auth/sessions.ts +26 -27
- package/src/auth/tables.ts +4 -0
- package/src/auth/verification.ts +61 -1
- package/src/cli/dev-app-runner.ts +4 -0
- package/src/cli/docker.ts +4 -1
- package/src/cli/load-config.images.test.ts +4 -0
- package/src/cli/load-config.ts +3 -0
- package/src/cli/registry.ts +1 -1
- package/src/console/server/operator-db.ts +34 -9
- package/src/docker/compose.ts +162 -6
- package/src/docker/derive.ts +60 -3
- package/src/docker/docker.test.ts +374 -1
- package/src/docker/helpers.ts +2 -0
- package/src/docker/index.ts +11 -0
- package/src/docker/recipes/caddy.ts +51 -0
- package/src/docker/recipes/dragonfly.ts +31 -0
- package/src/docker/recipes/index.ts +25 -2
- package/src/docker/recipes/pgdog.ts +84 -0
- package/src/docker/recipes/redis.ts +6 -3
- package/src/docker/recipes/traefik.ts +83 -0
- package/src/docker/recipes/valkey.ts +30 -0
- package/src/docker/stack-id.ts +5 -0
- package/src/docker/types.ts +18 -0
- package/src/drivers/channel-taqnyat-whatsapp.ts +94 -0
- package/src/drivers/channel-types.ts +1 -0
- package/src/elements/channel/otp-delivery.test.ts +76 -0
- package/src/elements/channel/otp-delivery.ts +291 -0
- package/src/elements/channel/runtime.ts +152 -114
- package/src/elements/channel.ts +12 -2
- package/src/index.ts +3 -0
- package/src/kernel/app.ts +60 -4
- package/src/kernel/boot-bind/channel.ts +51 -0
- package/src/kernel/boot-bind/gate.ts +14 -19
- package/src/kernel/boot-bind/honor-config.test.ts +18 -0
- package/src/kernel/boot-bind/signal.ts +20 -0
- package/src/kernel/boot-bind/store.test.ts +82 -0
- package/src/kernel/boot-bind/store.ts +22 -0
- package/src/kernel/boot.test.ts +5 -5
- package/src/kernel/fx.test.ts +3 -0
- package/src/kernel/fx.ts +49 -0
- package/src/kernel/graceful-shutdown.test.ts +76 -0
- package/src/kernel/graceful-shutdown.ts +106 -0
- package/src/kernel/horizontal-child.ts +257 -0
- package/src/kernel/horizontal.integration.test.ts +229 -0
- package/src/kernel/index.ts +8 -0
- package/src/kernel/ready.test.ts +76 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +5 -5
- package/src/plugins/auth-methods.security.test.ts +19 -29
- package/src/plugins/auth-methods.test.ts +7 -6
- package/src/plugins/index.ts +12 -8
- package/src/plugins/magic-link.ts +1 -23
- package/src/plugins/otp.test.ts +236 -0
- package/src/plugins/otp.ts +570 -0
- package/src/plugins/taqnyat.live.test.ts +4 -6
- package/src/release/official-plugins.ts +1 -2
- package/site/content/docs/plugins/email-otp.mdx +0 -117
- package/site/content/docs/plugins/phone-number.mdx +0 -172
- package/src/plugins/email-otp.ts +0 -214
- package/src/plugins/phone-number.ts +0 -206
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Kubernetes
|
|
3
|
+
description: Run an OKE app as a plain Deployment — PgDog, readiness vs liveness probes, graceful SIGTERM, shared drivers, and honest multi-instance limits.
|
|
4
|
+
icon: Container
|
|
5
|
+
source: README.md
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
OKE apps are safe to run as more than one replica when shared drivers back
|
|
9
|
+
Clock, Journal, Gate, and Store. Kubernetes is full orchestration with the
|
|
10
|
+
largest ecosystem — use a **Deployment**, never a StatefulSet.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Treat every replica as replaceable. Put durable state in Postgres / Redis / S3, probe `GET
|
|
14
|
+
/_/ready` for readiness and app `GET /health` for liveness, and set
|
|
15
|
+
`terminationGracePeriodSeconds` high enough for lease release on SIGTERM.
|
|
16
|
+
</Callout>
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
<Steps>
|
|
21
|
+
|
|
22
|
+
<Step>
|
|
23
|
+
### Deploy as a Deployment
|
|
24
|
+
|
|
25
|
+
Use a plain Kubernetes **Deployment** (or compatible replica set). Do **not**
|
|
26
|
+
use a StatefulSet — there is no per-pod volume identity, ordered startup, or
|
|
27
|
+
sticky network id to preserve. Scale with `replicas: N`.
|
|
28
|
+
|
|
29
|
+
</Step>
|
|
30
|
+
|
|
31
|
+
<Step>
|
|
32
|
+
### Share backing services
|
|
33
|
+
|
|
34
|
+
Pin docker/prod drivers to shared stores:
|
|
35
|
+
|
|
36
|
+
| Concern | Driver | Shared backend |
|
|
37
|
+
| ----------------------------- | ---------------------------- | ------------------------------------------------- |
|
|
38
|
+
| SQL / durable journal / clock | `postgres` | Postgres (optionally via PgDog) |
|
|
39
|
+
| Gate rate counters | `redis` (`drivers.store.kv`) | Redis / Valkey / Dragonfly (`images["store.kv"]`) |
|
|
40
|
+
| Files | `s3` | S3-compatible object store |
|
|
41
|
+
| Signal | see known limits below | — |
|
|
42
|
+
|
|
43
|
+
</Step>
|
|
44
|
+
|
|
45
|
+
<Step>
|
|
46
|
+
### Probe readiness and liveness
|
|
47
|
+
|
|
48
|
+
- **Readiness:** `GET /_/ready` — returns `200 { ready: true }` only after boot and the durable orphan scan finish. While booting or scanning orphans it returns `503 { ready: false, reason: "booting" | "orphan_scan" }`.
|
|
49
|
+
- **Liveness:** app-authored `GET /health` (create-oke ships one) — cheap “process is alive,” not orphan-scan aware.
|
|
50
|
+
|
|
51
|
+
Wire `readinessProbe` to `/_/ready` and `livenessProbe` to `/health`.
|
|
52
|
+
|
|
53
|
+
</Step>
|
|
54
|
+
|
|
55
|
+
</Steps>
|
|
56
|
+
|
|
57
|
+
## PgDog pooling
|
|
58
|
+
|
|
59
|
+
Bun.SQL defaults to **10** connections per process — fine for one pod.
|
|
60
|
+
Scale the Deployment and `N × pool` can exceed Postgres `max_connections`.
|
|
61
|
+
|
|
62
|
+
When both `store.sql` and `pgdog` are pinned (docker/prod default in templates),
|
|
63
|
+
`DATABASE_URL` points at PgDog on port `6432` — transaction pooling, wire-protocol
|
|
64
|
+
transparent to Bun.SQL / Drizzle. Point the cluster Service at that pooler.
|
|
65
|
+
|
|
66
|
+
**Why PgDog.** Transaction pooling fixes the connection math. Naive poolers can
|
|
67
|
+
leak session state (`SET`, RLS vars, `LISTEN`/`NOTIFY`) across clients; PgDog
|
|
68
|
+
re-applies those under transaction mode.
|
|
69
|
+
|
|
70
|
+
Read-replica routing (`BEGIN READ ONLY` → replica) is documented readiness —
|
|
71
|
+
not configured as a generated Kubernetes manifest this round.
|
|
72
|
+
|
|
73
|
+
## Redis-protocol images (Redis · Valkey · Dragonfly)
|
|
74
|
+
|
|
75
|
+
`images["store.kv"]` defaults to Redis (most mature). Pin Valkey or Dragonfly
|
|
76
|
+
the same way — driver id stays `redis`, same `REDIS_URL`. Image table + one-line
|
|
77
|
+
licenses: [Store · KV](/docs/elements/store#kv).
|
|
78
|
+
|
|
79
|
+
## Readiness vs liveness
|
|
80
|
+
|
|
81
|
+
| Endpoint | Role | Kubernetes |
|
|
82
|
+
| -------------- | ------------------------------------------------------ | ------------------------------------------------ |
|
|
83
|
+
| `GET /_/ready` | Kernel readiness (`booting` → `orphan_scan` → `ready`) | `readinessProbe` — no traffic until Ready |
|
|
84
|
+
| `GET /health` | App liveness (create-oke) — cheap “process alive” | `livenessProbe` — restart if the process is dead |
|
|
85
|
+
|
|
86
|
+
**Consequence:** orphan scan delays Ready (and traffic) on purpose so a new
|
|
87
|
+
pod is not hit mid-takeover. Do not point liveness at `/_/ready` — a long orphan
|
|
88
|
+
resume would kill the pod instead of waiting.
|
|
89
|
+
|
|
90
|
+
## Graceful shutdown
|
|
91
|
+
|
|
92
|
+
On `SIGTERM` / `SIGINT`, `installGracefulShutdown` (wired by `oke dev`’s app
|
|
93
|
+
runner; call it next to `createBunRuntime().serve` in custom entries):
|
|
94
|
+
|
|
95
|
+
1. Stops accepting new connections
|
|
96
|
+
2. Releases **Clock** cron leases and **Journal** run leases held by this instance
|
|
97
|
+
3. Closes element runtimes and exits
|
|
98
|
+
|
|
99
|
+
Signal message leases have no proactive release today — survivors reclaim after
|
|
100
|
+
the visibility TTL (default 30s). Set `terminationGracePeriodSeconds` ≥ your
|
|
101
|
+
longest lease TTL (typically ≥ 30s) so release finishes before kubelet SIGKILL.
|
|
102
|
+
|
|
103
|
+
## Ingress (TLS / routing)
|
|
104
|
+
|
|
105
|
+
Use cluster-native **Ingress** or Gateway API for HTTPS.
|
|
106
|
+
Compose `images.proxy` recipes (Caddy / Traefik) target plain Docker and Swarm —
|
|
107
|
+
see [Reverse proxy](/docs/deployment/reverse-proxy) when you are not on Kubernetes.
|
|
108
|
+
|
|
109
|
+
Still set serve `allowedHosts` to the public hostname
|
|
110
|
+
([Security](/docs/reference/security)).
|
|
111
|
+
|
|
112
|
+
## Known multi-instance limits
|
|
113
|
+
|
|
114
|
+
Be honest about what is still process-local:
|
|
115
|
+
|
|
116
|
+
| Surface | Multi-instance today |
|
|
117
|
+
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
118
|
+
| Clock (`postgres` / shared `file`) | Shared leader election |
|
|
119
|
+
| Journal (`postgres` / shared `file`) | Shared durable runs + orphan resume |
|
|
120
|
+
| Gate rates (`drivers.store.kv: redis`) | Shared counters |
|
|
121
|
+
| Store SQL / KV redis / files `s3` | Shared |
|
|
122
|
+
| Store files `fs` | **Per-instance filesystem** — boot warns; use `s3` when scaled |
|
|
123
|
+
| Signal `redis` | Emit relays to Redis; **consume / `live` / `drain` are process-local outbox** until Streams consume ships |
|
|
124
|
+
| Channel suppression / consent / receipts | **Default stores are process-local** — opt-out on replica A is invisible to B until a durable driver ships |
|
|
125
|
+
|
|
126
|
+
## Troubleshooting
|
|
127
|
+
|
|
128
|
+
<Accordions>
|
|
129
|
+
<Accordion title="Pods pass liveness but get no traffic after deploy">
|
|
130
|
+
|
|
131
|
+
Check readiness: `/_/ready` stays `503` with `reason: "orphan_scan"` until the
|
|
132
|
+
durable orphan resume finishes. A long resume delays Ready; that is intentional
|
|
133
|
+
so traffic does not hit a pod mid-takeover.
|
|
134
|
+
|
|
135
|
+
</Accordion>
|
|
136
|
+
<Accordion title="Rate limits look twice as high after scaling to 2">
|
|
137
|
+
|
|
138
|
+
`drivers.store.kv` must be `redis` with `REDIS_URL` set. A missing URL now
|
|
139
|
+
**fails boot** (no silent memory fallback). Declaring `memory` for local is fine;
|
|
140
|
+
docker/prod should stay on redis.
|
|
141
|
+
|
|
142
|
+
</Accordion>
|
|
143
|
+
<Accordion title="Files uploaded on pod A are missing on pod B">
|
|
144
|
+
|
|
145
|
+
`drivers.store.files: "fs"` is single-host. Switch docker/prod to `s3`
|
|
146
|
+
(create-oke templates already do).
|
|
147
|
+
|
|
148
|
+
</Accordion>
|
|
149
|
+
<Accordion title="Pod killed during orphan scan">
|
|
150
|
+
|
|
151
|
+
Liveness is pointed at `/_/ready`. Move liveness to `/health`; keep readiness
|
|
152
|
+
on `/_/ready` so the pod stays Alive while Waiting for Ready.
|
|
153
|
+
|
|
154
|
+
</Accordion>
|
|
155
|
+
</Accordions>
|
|
156
|
+
|
|
157
|
+
## Learn more
|
|
158
|
+
|
|
159
|
+
- [Deployment](/docs/deployment) — choose compose, Swarm, or Kubernetes
|
|
160
|
+
- [Docker](/docs/deployment/docker) — plain compose
|
|
161
|
+
- [Docker Swarm](/docs/deployment/docker-swarm) — `docker stack deploy`
|
|
162
|
+
- [Reverse proxy](/docs/deployment/reverse-proxy) — Caddy / Traefik (non-K8s)
|
|
163
|
+
- [Store](/docs/elements/store) — PgDog and facets
|
|
164
|
+
- [Clock](/docs/elements/clock) — shared CronStore / leader election
|
|
165
|
+
- [Flow](/docs/elements/flow) — durable journal
|
|
166
|
+
- [Signal](/docs/elements/signal) — delivery modes and redis honesty
|
|
167
|
+
- [Channel](/docs/elements/channel) — suppression process-local limit
|
|
168
|
+
- [CLI](/docs/reference/cli) — `oke docker --prod`, `oke start`
|
|
169
|
+
|
|
170
|
+
## Next
|
|
171
|
+
|
|
172
|
+
<Cards>
|
|
173
|
+
<Card title="Deployment" href="/docs/deployment" />
|
|
174
|
+
<Card title="Docker" href="/docs/deployment/docker" />
|
|
175
|
+
<Card title="Docker Swarm" href="/docs/deployment/docker-swarm" />
|
|
176
|
+
</Cards>
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Reverse proxy
|
|
3
|
+
description: Opt-in Caddy or Traefik for TLS termination and routing on plain Docker and Swarm — custom domains, Let's Encrypt, and the socket-proxy security model.
|
|
4
|
+
icon: Globe
|
|
5
|
+
source: README.md
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Pin `images.proxy` when you need HTTPS at the edge on plain `docker compose` or
|
|
9
|
+
Docker Swarm. Kubernetes uses cluster Ingress / Gateway API instead — this page
|
|
10
|
+
is for the compose recipes.
|
|
11
|
+
|
|
12
|
+
<Callout title="The one rule">
|
|
13
|
+
Leave the no-proxy default until you need TLS or horizontal scale on compose. Then pick **Caddy**
|
|
14
|
+
(simplest automatic HTTPS) or **Traefik** (label auto-discovery for `--scale app=N`). Never mount
|
|
15
|
+
the raw Docker socket into Traefik.
|
|
16
|
+
</Callout>
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
<Steps>
|
|
21
|
+
|
|
22
|
+
<Step>
|
|
23
|
+
### Opt in (pick one)
|
|
24
|
+
|
|
25
|
+
```typescript title="oke.config.ts"
|
|
26
|
+
images: {
|
|
27
|
+
"store.sql": "postgres:18-alpine",
|
|
28
|
+
// …
|
|
29
|
+
proxy: "caddy:2-alpine", // or "traefik:v3.3"
|
|
30
|
+
},
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Pin **either** Caddy or Traefik — never both. Re-run `oke docker --prod`.
|
|
34
|
+
|
|
35
|
+
</Step>
|
|
36
|
+
|
|
37
|
+
<Step>
|
|
38
|
+
### Set the public host and ACME email
|
|
39
|
+
|
|
40
|
+
```bash title="docker/.env.docker"
|
|
41
|
+
OKE_PROXY_HOST=app.example.com
|
|
42
|
+
OKE_PROXY_ACME_EMAIL=admin@example.com
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`.env.docker` is preserved across regeneration. Point DNS `A`/`AAAA` at the host.
|
|
46
|
+
|
|
47
|
+
</Step>
|
|
48
|
+
|
|
49
|
+
<Step>
|
|
50
|
+
### Bring the stack up
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cd docker
|
|
54
|
+
docker compose -f compose.yml -f compose.store.sql.yml \
|
|
55
|
+
-f compose.proxy.yml -f compose.prod.yml up -d
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Pass every `-f` file `oke docker` listed. The edge publishes `80`/`443`; `app`
|
|
59
|
+
stays on the internal `oke` network (no host bind on `6530`).
|
|
60
|
+
|
|
61
|
+
Add your public hostname to serve `allowedHosts`
|
|
62
|
+
([Security](/docs/reference/security)).
|
|
63
|
+
|
|
64
|
+
</Step>
|
|
65
|
+
|
|
66
|
+
</Steps>
|
|
67
|
+
|
|
68
|
+
## Which proxy?
|
|
69
|
+
|
|
70
|
+
| Choice | Best for | How routing works |
|
|
71
|
+
| ---------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
72
|
+
| **Caddy** (`caddy:2-alpine`) | hello / minimal / standard — single instance, simplest TLS | Generated `Caddyfile` → `reverse_proxy app:6530`; automatic HTTPS |
|
|
73
|
+
| **Traefik** (`traefik:v3.3`) | Horizontally scaled plain compose | Docker labels on `app`; replicas auto-discovered and load-balanced |
|
|
74
|
+
| _(omit `images.proxy`)_ | Local / private networks | App publishes `6530` — **default, unchanged** |
|
|
75
|
+
|
|
76
|
+
**Consequence:** Caddy is the recommended default when you just need a certificate.
|
|
77
|
+
Choose Traefik when `docker compose up --scale app=N` is the deployment shape.
|
|
78
|
+
|
|
79
|
+
## Caddy
|
|
80
|
+
|
|
81
|
+
Emits `docker/Caddyfile`:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
{$OKE_PROXY_HOST:localhost} {
|
|
85
|
+
reverse_proxy app:6530
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
| Setting | What it does |
|
|
90
|
+
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
91
|
+
| `OKE_PROXY_HOST` | Site address. `localhost` → local TLS; a public DNS name → Let's Encrypt HTTP-01 |
|
|
92
|
+
| `OKE_PROXY_ACME_EMAIL` | Written into `.env.docker` for the proxy role. Traefik consumes it; for Caddy, add a global `email` block if you want a specific ACME contact |
|
|
93
|
+
|
|
94
|
+
Custom domain:
|
|
95
|
+
|
|
96
|
+
1. DNS for `app.example.com` → this host
|
|
97
|
+
2. `OKE_PROXY_HOST=app.example.com` in `docker/.env.docker`
|
|
98
|
+
3. Ports `80`/`443` reachable from the public internet
|
|
99
|
+
4. `allowedHosts` includes `app.example.com`
|
|
100
|
+
|
|
101
|
+
Override the file via `compose.override.yml` or replace `docker/Caddyfile` —
|
|
102
|
+
`oke docker` regenerates the default on the next run.
|
|
103
|
+
|
|
104
|
+
## Traefik
|
|
105
|
+
|
|
106
|
+
Generated `compose.proxy.yml` labels `app`:
|
|
107
|
+
|
|
108
|
+
| Label | Meaning |
|
|
109
|
+
| --------------------------------------------------------- | -------------------------------------------- |
|
|
110
|
+
| `traefik.enable=true` | Advertise this service to Traefik |
|
|
111
|
+
| `traefik.http.routers.app.rule=Host(…)` | Match `OKE_PROXY_HOST` (default `localhost`) |
|
|
112
|
+
| `traefik.http.routers.app.entrypoints=websecure` | HTTPS entrypoint |
|
|
113
|
+
| `traefik.http.routers.app.tls.certresolver=letsencrypt` | ACME via Let's Encrypt |
|
|
114
|
+
| `traefik.http.services.app.loadbalancer.server.port=6530` | Upstream port on the compose network |
|
|
115
|
+
|
|
116
|
+
HTTP (`:80`) redirects to HTTPS. ACME email comes from
|
|
117
|
+
`OKE_PROXY_ACME_EMAIL` (default `admin@example.com`) on the Traefik command line.
|
|
118
|
+
|
|
119
|
+
Scaled replicas need no label edits — `docker compose up --scale app=N` registers
|
|
120
|
+
every replica through the Docker provider.
|
|
121
|
+
|
|
122
|
+
## Why docker-socket-proxy (Traefik)
|
|
123
|
+
|
|
124
|
+
Traefik's Docker provider watches the Engine API for containers and labels.
|
|
125
|
+
The naive setup mounts `/var/run/docker.sock` into Traefik itself.
|
|
126
|
+
|
|
127
|
+
That socket is root-equivalent for the host Docker daemon. If the edge process
|
|
128
|
+
is compromised (public `80`/`443`), an attacker with the raw socket can start
|
|
129
|
+
privileged containers, mount the host filesystem, or escape the container.
|
|
130
|
+
|
|
131
|
+
Traefik [documents this risk](https://doc.traefik.io/traefik/reference/install-configuration/providers/docker/)
|
|
132
|
+
and recommends a filtered proxy. OKE **never** mounts `docker.sock` into Traefik —
|
|
133
|
+
a companion `tecnativa/docker-socket-proxy` mounts it read-only and exposes only:
|
|
134
|
+
|
|
135
|
+
| Flag | Why Traefik needs it |
|
|
136
|
+
| ------------ | -------------------------- |
|
|
137
|
+
| `CONTAINERS` | List / inspect for labels |
|
|
138
|
+
| `EVENTS` | Watch start/stop for scale |
|
|
139
|
+
| `PING` | Health of the Engine API |
|
|
140
|
+
| `VERSION` | API negotiation |
|
|
141
|
+
| `NETWORKS` | Network membership |
|
|
142
|
+
|
|
143
|
+
Destructive Engine calls (`images`, `volumes`, exec, swarm admin, …) stay denied.
|
|
144
|
+
Traefik uses `--providers.docker.endpoint=tcp://socket-proxy:2375` on the
|
|
145
|
+
internal `oke` network only — do **not** publish `2375` on the host.
|
|
146
|
+
|
|
147
|
+
**Consequence:** a compromised Traefik still talks to a narrow API surface, not
|
|
148
|
+
the full Docker control plane.
|
|
149
|
+
|
|
150
|
+
## Environment
|
|
151
|
+
|
|
152
|
+
| Variable | Default | Used by |
|
|
153
|
+
| ---------------------- | ------------------- | ----------------------------------------- |
|
|
154
|
+
| `OKE_PROXY_HOST` | `localhost` | Caddy site address; Traefik `Host()` rule |
|
|
155
|
+
| `OKE_PROXY_ACME_EMAIL` | `admin@example.com` | Traefik ACME account email |
|
|
156
|
+
|
|
157
|
+
## Where this applies
|
|
158
|
+
|
|
159
|
+
| Surface | Edge / ingress |
|
|
160
|
+
| --------------------------------------------- | -------------------------------------------------------- |
|
|
161
|
+
| [Docker](/docs/deployment/docker) | Opt-in Caddy or Traefik (`images.proxy`) |
|
|
162
|
+
| [Docker Swarm](/docs/deployment/docker-swarm) | Same proxy recipes + Swarm routing mesh |
|
|
163
|
+
| [Kubernetes](/docs/deployment/kubernetes) | Ingress / Gateway API (cluster-native — not this recipe) |
|
|
164
|
+
|
|
165
|
+
## Troubleshooting
|
|
166
|
+
|
|
167
|
+
<Accordions>
|
|
168
|
+
<Accordion title="compose up --scale app=N fails with port already allocated">
|
|
169
|
+
|
|
170
|
+
You still have `6530:6530` on `app`. Confirm `images.proxy` is set and
|
|
171
|
+
`compose.proxy.yml` is in the `-f` list so derivation omitted the host bind.
|
|
172
|
+
|
|
173
|
+
</Accordion>
|
|
174
|
+
<Accordion title="Let's Encrypt challenge fails">
|
|
175
|
+
|
|
176
|
+
`OKE_PROXY_HOST` must be a public DNS name pointing at this host; ports `80`/`443`
|
|
177
|
+
reachable. Set `OKE_PROXY_ACME_EMAIL` for Traefik. For localhost-only smoke tests, prefer Caddy.
|
|
178
|
+
|
|
179
|
+
</Accordion>
|
|
180
|
+
<Accordion title="Traefik starts but never routes to app">
|
|
181
|
+
|
|
182
|
+
Confirm app labels are present (`traefik.enable=true`) in `compose.proxy.yml`,
|
|
183
|
+
containers share the `oke` network, and `socket-proxy` is healthy. Traefik must
|
|
184
|
+
not mount `docker.sock` itself.
|
|
185
|
+
|
|
186
|
+
</Accordion>
|
|
187
|
+
<Accordion title="Host header rejected (403)">
|
|
188
|
+
|
|
189
|
+
Serve validates `Host`. Add your public hostname to `allowedHosts`
|
|
190
|
+
([Security](/docs/reference/security)).
|
|
191
|
+
|
|
192
|
+
</Accordion>
|
|
193
|
+
<Accordion title="Caddy serves localhost certs on my public host">
|
|
194
|
+
|
|
195
|
+
`OKE_PROXY_HOST` is still `localhost` (or unset). Set the public DNS name in
|
|
196
|
+
`docker/.env.docker` and recreate the proxy container so it reloads the Caddyfile.
|
|
197
|
+
|
|
198
|
+
</Accordion>
|
|
199
|
+
</Accordions>
|
|
200
|
+
|
|
201
|
+
## Learn more
|
|
202
|
+
|
|
203
|
+
- [Deployment](/docs/deployment) — pick compose, Swarm, or Kubernetes
|
|
204
|
+
- [Docker](/docs/deployment/docker) — plain compose path (includes proxy opt-in)
|
|
205
|
+
- [Docker Swarm](/docs/deployment/docker-swarm) — `docker stack deploy`
|
|
206
|
+
- [CLI](/docs/reference/cli) — `oke docker`, `oke docker --prod`
|
|
207
|
+
- [Configuration](/docs/reference/configuration) — `images` pins
|
|
208
|
+
- [Security](/docs/reference/security) — `allowedHosts` behind a reverse proxy
|
|
209
|
+
|
|
210
|
+
## Next
|
|
211
|
+
|
|
212
|
+
<Cards>
|
|
213
|
+
<Card title="Deployment" href="/docs/deployment" />
|
|
214
|
+
<Card title="Docker" href="/docs/deployment/docker" />
|
|
215
|
+
<Card title="Docker Swarm" href="/docs/deployment/docker-swarm" />
|
|
216
|
+
</Cards>
|
|
@@ -85,6 +85,8 @@ Locally the `console` driver captures mail into an inbox instead of sending; in
|
|
|
85
85
|
|
|
86
86
|
Opt-out is first-class: a subject who opted out of a medium is **suppressed** — the send resolves without contacting the provider, and the receipt status is `suppressed/opted-out`. You never hand-roll "did they unsubscribe?" checks.
|
|
87
87
|
|
|
88
|
+
**Known limit:** default suppression, consent, and receipt stores are **process-local memory**. Opt-out / hard-bounce / receipt on replica A is invisible to replica B. Boot prints a one-shot warning when those defaults are used. Inject shared stores for multi-instance, or run a single Channel consumer, until a durable driver ships.
|
|
89
|
+
|
|
88
90
|
### Locale resolves, then the catalog
|
|
89
91
|
|
|
90
92
|
`fx.send` picks a locale: explicit `locale` → `profileLocale` → first
|
|
@@ -158,7 +160,7 @@ capability-gated `send` effects and dry-run safe.
|
|
|
158
160
|
|
|
159
161
|
They dispatch only when the bound SMS driver supports Verify (`taqnyat`); any other SMS
|
|
160
162
|
driver fails loudly instead of silently falling back to a self-generated code. The
|
|
161
|
-
[
|
|
163
|
+
[OTP](/docs/plugins/otp) Tier 1 uses this path automatically.
|
|
162
164
|
|
|
163
165
|
`webpush` needs VAPID keys — open it yourself and include it in
|
|
164
166
|
`BootOptions.channel.drivers` (boot does not open push from env):
|
|
@@ -245,14 +245,16 @@ drivers: {
|
|
|
245
245
|
},
|
|
246
246
|
```
|
|
247
247
|
|
|
248
|
-
| Driver | Runs as
|
|
249
|
-
| ---------- |
|
|
250
|
-
| `memory` | in-process
|
|
251
|
-
| `redis` | Redis / Valkey
|
|
252
|
-
| `postgres` | Postgres
|
|
253
|
-
| `nats` | NATS
|
|
248
|
+
| Driver | Runs as | Boot today |
|
|
249
|
+
| ---------- | ------------------------------------ | --------------------------------------------------------------------------- |
|
|
250
|
+
| `memory` | in-process | Local loop + tests — zero infrastructure |
|
|
251
|
+
| `redis` | Redis / Valkey / Dragonfly image pin | Boot-bound — **emit relay** via Streams / pub/sub; consume is process-local |
|
|
252
|
+
| `postgres` | Postgres | Driver exists; boot **fails loud** until a LISTEN/NOTIFY SQL client binds |
|
|
253
|
+
| `nats` | NATS | Driver exists; boot **fails loud** until a production NATS client binds |
|
|
254
254
|
|
|
255
|
-
`redis`
|
|
255
|
+
**Known limit:** `redis` relays emits to Redis, but `once` / `broadcast` / `live` **consume, `live` replay, and `drain` still use a process-local outbox** today. Competing consumers across replicas do not share that outbox until Redis Streams consume ships. Boot prints a one-shot warning when `drivers.signal` is `redis`. Prefer a single consumer instance (or a shared durable outbox path in tests) for multi-process `once` until then.
|
|
256
|
+
|
|
257
|
+
Pairing an arbitrary Store insert with emit inside **one** shared SQL transaction is not what `fx.emit` does — keep consumers idempotent (at-least-once).
|
|
256
258
|
|
|
257
259
|
## Troubleshooting
|
|
258
260
|
|
|
@@ -279,7 +281,7 @@ Ask: _how many consumers should process each message?_ One → `once`. All of th
|
|
|
279
281
|
</Accordion>
|
|
280
282
|
<Accordion title="My emit happened but the row didn't (or vice versa)">
|
|
281
283
|
|
|
282
|
-
`fx.emit` commits the signal outbox when it resolves; it does not wrap your Store insert. Treat delivery as at-least-once and make the consumer idempotent (or check the row before acting).
|
|
284
|
+
`fx.emit` commits the signal outbox when it resolves; it does not wrap your Store insert. Treat delivery as at-least-once and make the consumer idempotent (or check the row before acting). Remember: `redis` today is an emit relay with a process-local consume outbox — not multi-instance competing consumers yet.
|
|
283
285
|
|
|
284
286
|
</Accordion>
|
|
285
287
|
<Accordion title="The same once message ran twice">
|
|
@@ -251,6 +251,20 @@ Staging/prod accumulate versioned SQL under `drizzle/` (`oke db generate`).
|
|
|
251
251
|
| Local | `oke db push` (or auto-push from `oke dev`) |
|
|
252
252
|
| Staging / prod | `oke db generate` → review files → `oke db migrate` on that DB |
|
|
253
253
|
|
|
254
|
+
<Callout title="Connection pooling is infrastructure, not app code" type="info">
|
|
255
|
+
Bun.SQL defaults to **10** connections per process — fine per instance. Scale out and `N × pool`
|
|
256
|
+
can exceed Postgres `max_connections`. docker/prod puts **PgDog** in front; `DATABASE_URL` → port
|
|
257
|
+
`6432`. No app code changes.
|
|
258
|
+
</Callout>
|
|
259
|
+
|
|
260
|
+
**Why PgDog (not PgBouncer as the default).** Transaction pooling fixes the
|
|
261
|
+
math; naive poolers can leak session state (`SET`, RLS vars, `LISTEN`/`NOTIFY`)
|
|
262
|
+
across clients. PgDog re-applies those under transaction mode (PgBouncer is fine — not shipped).
|
|
263
|
+
|
|
264
|
+
**Read replicas later, zero app changes.** Add `role = "replica"` in
|
|
265
|
+
`pgdog.toml` later: `BEGIN READ ONLY` → replica, failover on promotion.
|
|
266
|
+
Not wired this round — readiness only.
|
|
267
|
+
|
|
254
268
|
#### Seeding
|
|
255
269
|
|
|
256
270
|
`oke db seed` loads `defineSeed` from `src/seed/index.ts` — **never at boot**.
|
|
@@ -490,6 +504,8 @@ drivers: {
|
|
|
490
504
|
},
|
|
491
505
|
images: {
|
|
492
506
|
"store.kv": "redis:8-alpine",
|
|
507
|
+
// or: "valkey/valkey:8-alpine"
|
|
508
|
+
// or: "docker.dragonflydb.io/dragonflydb/dragonfly"
|
|
493
509
|
},
|
|
494
510
|
```
|
|
495
511
|
|
|
@@ -505,6 +521,22 @@ images: {
|
|
|
505
521
|
|
|
506
522
|
Missing Redis URL fails boot loudly: `oke boot: redis driver needs REDIS_URL`.
|
|
507
523
|
|
|
524
|
+
Driver id stays `redis` for every image below — same `REDIS_URL`, zero Flow changes.
|
|
525
|
+
Redis is the default because it is the most mature and battle-tested; Valkey and
|
|
526
|
+
Dragonfly are equally legitimate opt-in pins.
|
|
527
|
+
|
|
528
|
+
| Image | Pin | Why pick it | License |
|
|
529
|
+
| ------------- | --------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
530
|
+
| **Redis** | `redis:8-alpine` (default) | Most mature / battle-tested | RSALv2 / SSPLv1 / AGPLv3 (Redis ≥8) — service limits under RSAL/SSPL apply if you offer Redis as a managed service to third parties |
|
|
531
|
+
| **Valkey** | `valkey/valkey:8-alpine` | BSD fork (Linux Foundation) | BSD-3-Clause — permissive; no managed-service restriction |
|
|
532
|
+
| **Dragonfly** | `docker.dragonflydb.io/dragonflydb/dragonfly` | Multi-threaded Redis-wire runtime | BSL 1.1 (converts to Apache 2.0 on a published change date) — free for self-hosting; restricts offering Dragonfly as a commercial managed service |
|
|
533
|
+
|
|
534
|
+
<Callout title="Licenses bite managed-service resellers — read this once" type="warn">
|
|
535
|
+
RSAL, SSPL, and BSL service restrictions apply when you offer that specific datastore **as a
|
|
536
|
+
service** to third parties — not when you run it for your own app. Pick for your situation; OKE
|
|
537
|
+
does not pick a “safer” default for you.
|
|
538
|
+
</Callout>
|
|
539
|
+
|
|
508
540
|
<Callout title="Dry-run refuses KV writes">
|
|
509
541
|
`set` and `delete` throw `DryRunWriteIsolationError` during dry-run — the runtime will not risk a
|
|
510
542
|
double-write against a shared Redis. Reads (`get` / `list`) still run.
|
|
@@ -603,6 +635,8 @@ images: {
|
|
|
603
635
|
|
|
604
636
|
`memory` is the test default. Local `fs` writes under a temp root when no binding `root` is set.
|
|
605
637
|
|
|
638
|
+
**Known limit:** `fs` is single-host. Under horizontal scale each replica sees its own filesystem (silently inconsistent object views). Boot prints a one-shot warning when `drivers.store.files` is `fs`. Use `s3` for docker/prod (create-oke templates already do).
|
|
639
|
+
|
|
606
640
|
<Callout title="Console can browse, not edit bytes">
|
|
607
641
|
The [Console · Store](/docs/console/store) lists keys and can delete them. Direct edit is **KV +
|
|
608
642
|
SQL only** — blob bodies are not patched from the Console.
|
|
@@ -30,4 +30,9 @@ Learn the shape once, install on Bun, then write your first Flow from the standa
|
|
|
30
30
|
description="Health Flow, typed client, Console proof."
|
|
31
31
|
href="/docs/get-started/basic-usage"
|
|
32
32
|
/>
|
|
33
|
+
<Card
|
|
34
|
+
title="Deployment"
|
|
35
|
+
description="Compose, Swarm, Kubernetes, and reverse proxy."
|
|
36
|
+
href="/docs/deployment"
|
|
37
|
+
/>
|
|
33
38
|
</Cards>
|
|
@@ -32,6 +32,11 @@ on(orderPlaced, sendReceipt);
|
|
|
32
32
|
href="/docs/plugins"
|
|
33
33
|
/>
|
|
34
34
|
<Card title="Console" description="Manifest-derived operator panels." href="/docs/console" />
|
|
35
|
+
<Card
|
|
36
|
+
title="Deployment"
|
|
37
|
+
description="Compose, Swarm, Kubernetes, reverse proxy."
|
|
38
|
+
href="/docs/deployment"
|
|
39
|
+
/>
|
|
35
40
|
<Card
|
|
36
41
|
title="Reference"
|
|
37
42
|
description="Config, fx, env, errors, CLI, security."
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"title": "Documentation",
|
|
3
3
|
"icon": "BookOpen",
|
|
4
|
-
"pages": [
|
|
4
|
+
"pages": [
|
|
5
|
+
"index",
|
|
6
|
+
"get-started",
|
|
7
|
+
"elements",
|
|
8
|
+
"plugins",
|
|
9
|
+
"console",
|
|
10
|
+
"deployment",
|
|
11
|
+
"reference",
|
|
12
|
+
"ai"
|
|
13
|
+
]
|
|
5
14
|
}
|
|
@@ -17,8 +17,7 @@ First-party plugins you `.plug()` onto an app. Each page is one export from `oke
|
|
|
17
17
|
href="/docs/plugins/anonymous"
|
|
18
18
|
/>
|
|
19
19
|
<Card title="Magic link" description="One-time email link." href="/docs/plugins/magic-link" />
|
|
20
|
-
<Card title="
|
|
21
|
-
<Card title="Phone number" description="E.164 SMS OTP." href="/docs/plugins/phone-number" />
|
|
20
|
+
<Card title="OTP" description="SMS, WhatsApp, or email codes." href="/docs/plugins/otp" />
|
|
22
21
|
<Card title="Two-factor" description="TOTP enable / verify." href="/docs/plugins/two-factor" />
|
|
23
22
|
<Card title="Passkey" description="WebAuthn-shaped passkeys." href="/docs/plugins/passkey" />
|
|
24
23
|
</Cards>
|
|
@@ -147,14 +147,14 @@ unit tests without SMTP, set `exposeDevToken: true`.
|
|
|
147
147
|
|
|
148
148
|
## Learn more
|
|
149
149
|
|
|
150
|
-
- [
|
|
150
|
+
- [OTP](/docs/plugins/otp) — numeric code instead of a link
|
|
151
151
|
- [Gate](/docs/elements/gate) — `gate.auth`
|
|
152
152
|
- [Channel](/docs/elements/channel) — `fx.send`, Mailpit, consents
|
|
153
153
|
|
|
154
154
|
## Next
|
|
155
155
|
|
|
156
156
|
<Cards>
|
|
157
|
-
<Card title="
|
|
157
|
+
<Card title="OTP" description="SMS, WhatsApp, or email codes." href="/docs/plugins/otp" />
|
|
158
158
|
<Card title="Gate" description="Builtin auth and policies." href="/docs/elements/gate" />
|
|
159
159
|
<Card title="Channel" description="Email delivery and Mailpit." href="/docs/elements/channel" />
|
|
160
160
|
</Cards>
|