Skip to content

Commit 34ee7ef

Browse files
danmolitorclaude
andcommitted
Add self-hosted server crate and docs
New `server/` crate — a minimal Axum HTTP server wrapping the Forme engine as a stateless Docker container. No database, no S3, no Redis. Endpoints: POST /v1/render (inline), POST /v1/render/:slug (file-based), POST /v1/sign (PDF signing), GET /health. Optional bearer token auth via FORME_API_KEY env var. Template directory mounting via FORME_TEMPLATES_DIR. Path traversal prevention on slug endpoint. Includes Dockerfile, docker-compose.yml, 19 tests, and a self-hosting docs page. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7f53a6e commit 34ee7ef

File tree

17 files changed

+3597
-1
lines changed

17 files changed

+3597
-1
lines changed

docs/docs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
},
2929
{
3030
"group": "Reference",
31-
"pages": ["components", "styles", "tailwind", "fonts", "page-breaks", "charts", "templates", "embedded-data", "api-reference"]
31+
"pages": ["components", "styles", "tailwind", "fonts", "page-breaks", "charts", "templates", "embedded-data", "api-reference", "self-hosting"]
3232
},
3333
{
3434
"group": "Enterprise",

docs/self-hosting.mdx

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
---
2+
title: Self-Hosting
3+
sidebarTitle: Self-Hosting
4+
description: Run the Forme render engine as a self-hosted Docker container. Same API, no external dependencies, no database, no Chromium.
5+
---
6+
7+
A single Docker container that exposes the Forme render engine as an HTTP API. Same endpoints as the [hosted API](/api-reference), runs anywhere Docker runs. No Chromium, no LibreOffice, no database, no native dependencies.
8+
9+
```bash
10+
docker run --rm -p 3000:3000 formepdf/forme:latest
11+
```
12+
13+
PDF API on port 3000. That's the whole install.
14+
15+
---
16+
17+
## Quick Start
18+
19+
### Docker run
20+
21+
```bash
22+
docker run --rm -p 3000:3000 formepdf/forme:latest
23+
```
24+
25+
### Docker Compose
26+
27+
```yaml docker-compose.yml
28+
version: "3.8"
29+
30+
services:
31+
forme:
32+
image: formepdf/forme:latest
33+
ports:
34+
- "3000:3000"
35+
environment:
36+
- FORME_API_KEY=your-secret-key # optional
37+
- FORME_TEMPLATES_DIR=/templates # optional
38+
volumes:
39+
- ./templates:/templates:ro
40+
restart: unless-stopped
41+
```
42+
43+
```bash
44+
docker compose up
45+
```
46+
47+
### Health check
48+
49+
```bash
50+
curl http://localhost:3000/health
51+
# {"status":"ok","version":"0.8.1"}
52+
```
53+
54+
---
55+
56+
## API Endpoints
57+
58+
The self-hosted API matches the hosted API, so switching between them is a base URL change — not a code change. All existing SDKs work against self-hosted without modification.
59+
60+
| Endpoint | Description |
61+
|----------|-------------|
62+
| `POST /v1/render` | Inline render — compiled template JSON + data in request body |
63+
| `POST /v1/render/:slug` | Template render — loads pre-compiled JSON from mounted volume |
64+
| `POST /v1/sign` | Sign an existing PDF with an X.509 certificate |
65+
| `GET /health` | Health check |
66+
67+
### Inline render
68+
69+
Pass the compiled template JSON and data directly in the request body. No template storage needed. Fully stateless.
70+
71+
<CodeGroup>
72+
73+
```bash curl
74+
curl -X POST http://localhost:3000/v1/render \
75+
-H "Content-Type: application/json" \
76+
-d '{
77+
"template": { ... },
78+
"data": { "customer": "Acme", "amount": 1500 }
79+
}' \
80+
--output invoice.pdf
81+
```
82+
83+
```javascript Node.js
84+
const res = await fetch("http://localhost:3000/v1/render", {
85+
method: "POST",
86+
headers: { "Content-Type": "application/json" },
87+
body: JSON.stringify({
88+
template: compiledTemplateJson,
89+
data: { customer: "Acme", amount: 1500 },
90+
}),
91+
});
92+
93+
const pdf = Buffer.from(await res.arrayBuffer());
94+
fs.writeFileSync("invoice.pdf", pdf);
95+
```
96+
97+
```python Python
98+
import requests
99+
100+
res = requests.post(
101+
"http://localhost:3000/v1/render",
102+
json={
103+
"template": compiled_template_json,
104+
"data": {"customer": "Acme", "amount": 1500},
105+
},
106+
)
107+
108+
with open("invoice.pdf", "wb") as f:
109+
f.write(res.content)
110+
```
111+
112+
</CodeGroup>
113+
114+
The `template` field is the compiled document JSON tree — the output of `forme build --template` or `serialize()` in `@formepdf/react`. The `data` field is optional; when present, Forme evaluates template expressions (`$ref`, `$each`, `$if`) against it before rendering.
115+
116+
### Template render
117+
118+
Mount a directory of pre-compiled JSON templates and reference them by slug (filename without `.json`).
119+
120+
```
121+
./templates/
122+
invoice.json
123+
contract.json
124+
report.json
125+
```
126+
127+
<CodeGroup>
128+
129+
```bash curl
130+
curl -X POST http://localhost:3000/v1/render/invoice \
131+
-H "Content-Type: application/json" \
132+
-d '{ "data": { "customer": "Acme", "amount": 1500 } }' \
133+
--output invoice.pdf
134+
```
135+
136+
```javascript Node.js
137+
const res = await fetch("http://localhost:3000/v1/render/invoice", {
138+
method: "POST",
139+
headers: { "Content-Type": "application/json" },
140+
body: JSON.stringify({
141+
data: { customer: "Acme", amount: 1500 },
142+
}),
143+
});
144+
```
145+
146+
```python Python
147+
res = requests.post(
148+
"http://localhost:3000/v1/render/invoice",
149+
json={"data": {"customer": "Acme", "amount": 1500}},
150+
)
151+
```
152+
153+
</CodeGroup>
154+
155+
Templates are loaded from disk on each request — no restart needed when templates change.
156+
157+
### Sign
158+
159+
```bash
160+
curl -X POST http://localhost:3000/v1/sign \
161+
-H "Content-Type: application/json" \
162+
-d '{
163+
"pdf": "<base64-encoded PDF>",
164+
"certificatePem": "-----BEGIN CERTIFICATE-----\n...",
165+
"privateKeyPem": "-----BEGIN PRIVATE KEY-----\n...",
166+
"reason": "Approved"
167+
}' \
168+
--output signed.pdf
169+
```
170+
171+
---
172+
173+
## Compiling Templates
174+
175+
The self-hosted container is a pure Rust binary with no Node.js runtime. This means `.tsx` templates must be compiled to JSON before mounting — the same way you'd compile TypeScript before deploying to production.
176+
177+
```bash
178+
# Compile a single template
179+
npx forme build --template invoice.tsx
180+
181+
# Output: invoice.json (compiled document tree)
182+
```
183+
184+
In your CI pipeline, add the build step alongside your existing TypeScript compilation:
185+
186+
```bash
187+
# Install dependencies
188+
npm install @formepdf/react @formepdf/cli
189+
190+
# Compile all templates
191+
for f in templates/*.tsx; do
192+
npx forme build --template "$f"
193+
done
194+
195+
# Deploy the .json files to your container volume
196+
```
197+
198+
The hosted API handles this compilation step for you — that's one of the conveniences of the paid tiers.
199+
200+
---
201+
202+
## Authentication
203+
204+
Optional. If `FORME_API_KEY` is not set, the API is open — suitable for local development or internal network use behind a firewall.
205+
206+
If set, all `/v1/*` endpoints require:
207+
208+
```
209+
Authorization: Bearer your-secret-key
210+
```
211+
212+
The `/health` endpoint is always public.
213+
214+
```bash
215+
# Start with auth enabled
216+
docker run --rm -p 3000:3000 -e FORME_API_KEY=my-secret formepdf/forme:latest
217+
218+
# Requests require the key
219+
curl -X POST http://localhost:3000/v1/render/invoice \
220+
-H "Authorization: Bearer my-secret" \
221+
-H "Content-Type: application/json" \
222+
-d '{ "data": { "customer": "Acme" } }' \
223+
--output invoice.pdf
224+
```
225+
226+
---
227+
228+
## Configuration
229+
230+
All configuration is via environment variables. No config files.
231+
232+
| Variable | Default | Description |
233+
|----------|---------|-------------|
234+
| `HTTP_PORT` | `3000` | Port the server listens on |
235+
| `FORME_API_KEY` | *(none)* | If set, enables Bearer token auth on all `/v1/*` endpoints |
236+
| `FORME_TEMPLATES_DIR` | *(none)* | Path to directory of pre-compiled `.json` templates |
237+
238+
---
239+
240+
## SDK Configuration
241+
242+
All Forme SDKs support a custom base URL. Point them at your self-hosted instance:
243+
244+
<CodeGroup>
245+
246+
```javascript Node.js
247+
import { FormeClient } from "@formepdf/sdk";
248+
const client = new FormeClient({
249+
baseUrl: "http://localhost:3000",
250+
apiKey: "your-key",
251+
});
252+
```
253+
254+
```python Python
255+
import formepdf
256+
client = formepdf.Client(base_url="http://localhost:3000", api_key="your-key")
257+
```
258+
259+
```go Go
260+
import forme "github.com/formepdf/forme-go"
261+
client := forme.New("your-key", forme.WithBaseURL("http://localhost:3000"))
262+
```
263+
264+
</CodeGroup>
265+
266+
---
267+
268+
## Deployment Examples
269+
270+
### Kubernetes
271+
272+
```yaml
273+
apiVersion: apps/v1
274+
kind: Deployment
275+
metadata:
276+
name: forme
277+
spec:
278+
replicas: 3
279+
selector:
280+
matchLabels:
281+
app: forme
282+
template:
283+
metadata:
284+
labels:
285+
app: forme
286+
spec:
287+
containers:
288+
- name: forme
289+
image: formepdf/forme:latest
290+
ports:
291+
- containerPort: 3000
292+
env:
293+
- name: FORME_API_KEY
294+
valueFrom:
295+
secretKeyRef:
296+
name: forme-secrets
297+
key: api-key
298+
```
299+
300+
Stateless — scales horizontally with no coordination needed.
301+
302+
### With templates in Kubernetes
303+
304+
Mount templates from a ConfigMap or persistent volume:
305+
306+
```yaml
307+
volumes:
308+
- name: templates
309+
configMap:
310+
name: forme-templates
311+
containers:
312+
- name: forme
313+
volumeMounts:
314+
- name: templates
315+
mountPath: /templates
316+
env:
317+
- name: FORME_TEMPLATES_DIR
318+
value: /templates
319+
```
320+
321+
---
322+
323+
## What Self-Hosted Does Not Include
324+
325+
These features are available on the [hosted API](https://api.formepdf.com) only:
326+
327+
- **Template compilation** — upload `.tsx` files directly, Forme compiles them server-side
328+
- **Dashboard** — template editor, usage graphs, logs, billing
329+
- **AI template generation** — Claude-powered template creation
330+
- **Team management** — org-level API keys, seat management
331+
- **Usage analytics** — per-render logging and analytics
332+
- **Async rendering** — job queuing, webhooks, S3 upload
333+
- **Priority support** — SLA-backed response times
334+
335+
Self-hosters who need these features can upgrade to the hosted service at any time — it's a base URL change.
336+
337+
---
338+
339+
## Comparison
340+
341+
| | Gotenberg | Forme Self-Hosted |
342+
|--|-----------|-------------------|
343+
| Dependencies | Chromium + LibreOffice | None |
344+
| Image size | ~1GB | ~50MB |
345+
| Cold start | 2-5s | <100ms |
346+
| Input | HTML, Markdown, Office docs | JSX templates (compiled to JSON) |
347+
| Page breaks | Chromium-dependent | Engine-native |
348+
| AcroForms | No | Yes |
349+
| Digital signatures | No | Yes |
350+
| PDF/UA | No | Yes |
351+
| PDF/A | No | Yes |
352+
353+
Gotenberg converts existing documents (HTML, Word, etc.). Forme generates from code templates. Different tools for different jobs.

server/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/target

0 commit comments

Comments
 (0)