Website form lead routing for Clay-powered backend.
Website form lead routing for Clay-powered backend
What it does
Greenware lets companies keep their existing forms while routing each submission through Clay before deciding what the visitor sees next.
Website form / Tally / Typeform
-> Greenware backend on Railway
-> Clay webhook
-> Clay runs enrichment and routing logic
-> Clay POSTs a signed callback back to Greenware
-> Browser receives an embed, redirect, message, or rejection
Greenware is not a form builder and does not ship a hosted marketing site. It is a backend service plus two optional JavaScript embed files:
/api/*routes for submission, provider ingest, session polling, and signed callbacks/embed/v1.jsfor browser/session orchestration/embed/v1-default-ui.jsfor the default spinner and action renderer
The deployable backend intentionally includes no HTML preview pages, demo pages, or hosted example website.
Getting started
Railway step-by-step
Click the Deploy on Railway button above
Sign in to Railway or create an account
If asked to name the project, pick anything you like, such as
greenwareLeave the variables as-is. Greenware can boot without any required deploy-time secrets
Deploy the template
Wait for the build to finish
Open the
greenwareservice. If it is unexposed, go to Settings -> Networking -> Public Networking and click Generate Domain. Copy the public Railway URL, such as:https://greenware-production-xxxx.up.railway.appOpen
/healthon the public URL. It should return{"status":"ok"}If the Variables tab says No Environment Variables, that is expected. Greenware is online, but not ready to route leads yet.
Continue to Step 2 and add the production variables for Clay and your website origins.
The Railway template creates one Greenware backend service. It intentionally does not create a database. See docs/railway-template.md for the exact template settings.
Local / VPS / other
git clone https://github.com/eliasstravik/greenware.git
cd greenware
bun install
cp apps/server/.env.example apps/server/.env
bun run build
bun run start
For live routing outside Railway, set:
GREENWARE_DESTINATIONS='{"default":{"webhook_url":"INSERT_WEBHOOK_URL_HERE","headers":{"x-clay-webhook-auth":"INSERT_AUTH_TOKEN_HERE"}}}'
GREENWARE_ALLOWED_ORIGINS=https://yourcompany.com,https://www.yourcompany.com
GREENWARE_ENV=production
GREENWARE_PUBLIC_URL=https://greenware.yourcompany.com
PORT=8080
# Optional stable keys.
GREENWARE_SIGNING_KEY=long-random-secret
GREENWARE_READ_KEY=long-random-secret
# Optional protected setup endpoint.
GREENWARE_SETUP_TOKEN=long-random-secret
Your server URL is whatever domain or IP points to this process.
Step 2: Make the deployment ready
A fresh deploy needs no variables to boot. To start routing real form submissions, open the Railway greenware service, go to Variables, and add the variables below. You can use Raw Editor to paste them in.
Required for Clay
GREENWARE_DESTINATIONS tells Greenware which Clay webhook to call.
GREENWARE_DESTINATIONS={"default":{"webhook_url":"INSERT_WEBHOOK_URL_HERE","headers":{"x-clay-webhook-auth":"INSERT_AUTH_TOKEN_HERE"}}}
Replace:
INSERT_WEBHOOK_URL_HEREwith your Clay webhook source URLINSERT_AUTH_TOKEN_HEREwith your Clay webhook auth token
Required for browser forms and embeds
GREENWARE_ALLOWED_ORIGINS tells Greenware which websites may call the browser APIs. Use exact origins only: scheme, hostname, and port if needed. Do not include paths.
GREENWARE_ALLOWED_ORIGINS=https://yourcompany.com,https://www.yourcompany.com
This is needed for custom-coded forms, Webflow/Squarespace/WordPress pages, and Tally or Typeform embeds that use Greenware's JavaScript. It is not needed for the provider's server-to-server webhook POST.
Origin checks protect browser integrations from accidental cross-site use. They are not server-to-server authentication, because non-browser clients can forge an Origin header. Keep provider webhook URLs unguessable through per-session hidden fields, and use Railway or service-level protections if you expose Greenware to high-volume public traffic.
After adding variables, redeploy the service and check:
curl https://your-greenware-url/health
curl https://your-greenware-url/ready
Expected /health:
{ "status": "ok" }
Expected /ready:
{ "status": "ok", "destination": "default", "storage": "memory" }
If /ready returns not_ready, GREENWARE_DESTINATIONS is missing or invalid, or your only allowed origins are local development origins / the Railway service domain. Add the real website origin that hosts the form.
Multiple Clay destinations
Greenware can route different forms to different Clay webhook tables. In Railway, open the Greenware service variables and set GREENWARE_DESTINATIONS as JSON.
{
"default": {
"webhook_url": "INSERT_DEFAULT_WEBHOOK_URL_HERE",
"headers": {
"x-clay-webhook-auth": "INSERT_DEFAULT_AUTH_TOKEN_HERE"
}
},
"enterprise-demo": {
"webhook_url": "INSERT_ENTERPRISE_WEBHOOK_URL_HERE",
"headers": {
"x-clay-webhook-auth": "INSERT_ENTERPRISE_AUTH_TOKEN_HERE"
}
},
"inbound-partners": {
"webhook_url": "INSERT_PARTNERS_WEBHOOK_URL_HERE",
"headers": {
"x-clay-webhook-auth": "INSERT_PARTNERS_AUTH_TOKEN_HERE"
},
"timeout_ms": 15000
}
}
If a submission has form_id: "enterprise-demo", Greenware sends it to the enterprise-demo destination. If no exact match exists, it uses default.
Step 3: Configure Clay callback
Every Greenware request sent to Clay includes:
{
"session_id": "6f2a7cba-5f63-4cf5-9f14-41c4f9c5b84c",
"callback_url": "https://your-greenware-url/api/callback/...",
"lead": {
"email": "alice@example.com"
},
"form_id": "enterprise-demo",
"source": {
"provider": "tally"
},
"meta": {
"submitted_at": "2026-05-20T10:00:00.000Z"
}
}
Add an HTTP API column in Clay as the last routing step.
Method: POST
URL:
{{callback_url}}
Headers:
- Key:
Content-Type - Value:
application/json
Body:
{
"session_id": "{{session_id}}",
"status": "ok",
"action": {
"type": "embed",
"provider": "calendly",
"url": "https://calendly.com/your-team/demo",
"mobile_behavior": "redirect"
},
"meta": {
"source": "clay"
}
}
Step 4: Use it from your form
Custom-coded forms
Add the Greenware attributes and scripts to an existing form:
<form
data-greenware-attach
data-greenware-endpoint="https://your-greenware-url"
data-greenware-form-id="enterprise-demo"
>
<input name="email" type="email" required>
<button type="submit">Book a demo</button>
</form>
<script src="https://your-greenware-url/embed/v1.js"></script>
<script src="https://your-greenware-url/embed/v1-default-ui.js"></script>
Use only /embed/v1.js if you want your own UI and listen for greenware:* events.
Tally, Typeform, and provider embeds
Use Greenware to create a session before loading the provider form. Greenware then passes these hidden fields into the provider URL or provider hidden-field settings:
greenware_session_id
greenware_read_token
greenware_form_id
Create hidden fields with exactly those names in Tally, Typeform, or the provider you use. Do not hard-code values into those fields. Greenware generates fresh values for each visitor session and appends them when it loads the embedded form.
For Tally embeds, Greenware can inject those values directly:
<div
data-greenware-provider="tally"
data-greenware-form-id="enterprise-demo"
data-greenware-iframe-src="https://tally.so/embed/YOUR_FORM_ID?alignLeft=1&hideTitle=1&transparentBackground=1&dynamicHeight=1"
></div>
<script src="https://your-greenware-url/embed/v1.js"></script>
<script src="https://your-greenware-url/embed/v1-default-ui.js"></script>
Configure the provider webhook:
https://your-greenware-url/api/ingest/tally
https://your-greenware-url/api/ingest/typeform
https://your-greenware-url/api/ingest/generic
Do not add an authorization header or ?token= query parameter to provider webhook URLs. Provider webhooks are authenticated by the per-session greenware_read_token hidden field. GREENWARE_SETUP_TOKEN is only needed if you want to enable protected setup endpoints such as /setup/sessions.
API reference
All endpoints return JSON except /wait/:sessionId, which is a minimal owned fallback page for redirect-only form providers.
GET /health
Railway health check. No auth.
curl https://your-greenware-url/health
{ "status": "ok" }
GET /ready
Configuration readiness check. No auth. Returns 503 until a real Clay destination and at least one non-local, non-Railway website origin are configured.
{ "status": "ok", "destination": "default", "storage": "memory" }
GET /
Backend metadata. No auth.
{
"name": "greenware",
"status": "ok",
"kind": "backend"
}
POST /api/submit
Accepts a custom-coded browser form submission. Requires the browser Origin to match GREENWARE_ALLOWED_ORIGINS or the Railway public domain. The endpoint is rate-limited per IP; treat GREENWARE_ALLOWED_ORIGINS as browser CORS protection, not as secret authentication.
curl -X POST https://your-greenware-url/api/submit \
-H "Origin: https://your-site.com" \
-H "Content-Type: application/json" \
-d '{
"lead": { "email": "alice@example.com" },
"form_id": "enterprise-demo"
}'
{
"session_id": "6f2a7cba-5f63-4cf5-9f14-41c4f9c5b84c",
"read_token": "...",
"expires_at": 1779283497
}
POST /api/session/start
Creates a pending session for Tally, Typeform, or another provider form before the provider submits.
{
"provider": "tally",
"form_id": "enterprise-demo"
}
POST /api/ingest/:provider
Receives provider webhooks. Supported providers are tally, typeform, and generic.
Provider submissions must include the Greenware hidden fields returned by /api/session/start:
greenware_session_id
greenware_read_token
greenware_form_id
POST /api/callback/:sessionId
Clay posts the final routing action to the signed callback_url Greenware sent in the Clay webhook body. Do not construct this URL yourself.
GET /api/session/:sessionId
Browser polling endpoint. Requires:
Authorization: Bearer READ_TOKEN
GET /setup/sessions
Protected operational view for recent terminal sessions. Requires the setup token and returns redacted metadata only.
curl https://your-greenware-url/setup/sessions \
-H "Authorization: Bearer INSERT_SETUP_TOKEN_HERE"
Action payloads
Redirect:
{ "type": "redirect", "url": "https://your-site.com/signup" }
Embed:
{
"type": "embed",
"provider": "calendly",
"url": "https://calendly.com/your-team/demo",
"mobile_behavior": "redirect"
}
Message:
{
"type": "message",
"title": "Thanks",
"body": "We will follow up by email."
}
Reject:
{
"type": "reject",
"reason": "Not a fit right now"
}
See docs/protocol-v1.md for the full schema.
Environment variables
Greenware is zero-config at deploy time. If signing/read keys are absent, the runtime uses ephemeral process-local keys, which is acceptable because sessions are short-lived and in memory. Set stable keys only if you want explicit key management across restarts.
| Variable | Required | Default | Description |
|---|---|---|---|
GREENWARE_SIGNING_KEY |
No | ephemeral runtime secret | HMAC key for callback URLs |
GREENWARE_READ_KEY |
No | ephemeral runtime secret | HMAC key for browser read tokens |
GREENWARE_SETUP_TOKEN |
No | disabled | Optional token for protected setup endpoints |
GREENWARE_DESTINATIONS |
Yes for live routing | none | JSON map of Clay webhook destinations |
GREENWARE_ALLOWED_ORIGINS |
Yes for browser integrations | local development origins only | Comma-separated browser origins allowed to call /api/submit, /api/session/start, and /api/session/* |
GREENWARE_PUBLIC_URL |
No | RAILWAY_PUBLIC_DOMAIN |
Canonical public URL used when Greenware builds signed callback URLs |
GREENWARE_ENV |
No | production |
Runtime environment |
GREENWARE_SIGNING_KEY_PREVIOUS |
No | Previous callback signing key during rotation | |
PORT |
No | 8787 locally, Railway-provided in deploy |
HTTP port |
Development
bun install
cp apps/server/.env.example apps/server/.env
bun run dev
Useful commands:
| Command | Description |
|---|---|
bun run dev |
Start the Bun server locally |
bun run build |
Type-check the server |
bun run typecheck |
Type-check without deployment |
bun run test |
Run the test suite |
bun run deploy |
Deploy the current workspace with railway up |
Architecture
Built with Hono, TypeScript, and Bun.
apps/server/src/
index.ts Hono app factory and public routes
server.ts Bun/Railway entrypoint
routes/submit.ts Custom form submit endpoint
routes/start.ts Provider session bootstrap endpoint
routes/ingest.ts Tally/Typeform/generic webhook ingest
routes/callback.ts Signed Clay callback endpoint
routes/session.ts Browser polling endpoint
lib/enrichment_destinations.ts Railway-configured Clay routing
lib/protocol.ts Callback action schema
lib/sessions.ts Session store interface and memory store
Railway runs one backend service. Sessions are short-lived in-memory records, so there is no Postgres, SQLite, Redis, or other database service to configure.
Security measures
- HMAC-signed callback URLs
- HMAC-derived read tokens for browser polling
- Exact-origin CORS checks for browser submit/session endpoints
- Optional setup token for protected operational endpoints
- HTTPS-only action URLs
- Strict Protocol v1 callback schema
- Optional key rotation through
GREENWARE_SIGNING_KEY_PREVIOUS
License
{
"request": {
"method": "GET",
"url": "https://api.eliasstravik.com/projects/greenware"
},
"response": {
"status": "200 OK",
"headers": {
"content-type": "application/json; charset=utf-8",
"x-powered-by": "one person + systems"
},
"body": {
"name": "greenware",
"description": "Website form lead routing for Clay-powered backend",
"language": "TypeScript",
"stars": 0,
"forks": 0,
"repo": "https://github.com/eliasstravik/greenware",
"readme": "https://github.com/eliasstravik/greenware#readme"
}
}
}