Guide
How Submify works
Submify is a self-hosted backend for HTML/JS forms. You create projects, each with a public and secret key. Browsers POST JSON to /api/submit with the public key; you review rows in the dashboard and export when needed.
Next.js marketing sites — For the server-only Nodedr submit proxy pattern (contact forms without exposing keys), see the dedicated Next.js contact proxy guide. If you use an AI coding assistant, that page includes a copy-paste prompt for implementing the same pattern elsewhere; read For AI builders there before changing routes so you do not break /api/submit vs /api/contact-submit.
Prompt you can reuse in chat (copy for AI assistants)
Prompt you can reuse in chat
Copy and adjust the bracketed parts:
In this repo's Next.js App Router site at [path/to/site-folder], implement contact form submission using the Nodedr submit API proxy pattern (same as SeattleDrainCleaningCo), not FormSubmit in the browser.
Requirements:
1. Add `src/app/api/submit/route.ts` that accepts POST JSON, validates with a shared Zod schema (honeypot field e.g. gotcha must be empty), builds the upstream JSON payload, and POSTs to `https://api.nodedr.com/api/submit` with `Content-Type: application/json`, header `x-api-key` set from server env (`NODEDR_SUBMIT_PUBLIC_KEY` or `NODEDR_PUBLIC_KEY`, value must be `pk_...`). If `NODEDR_SUBMIT_SECRET_KEY` (`sk_...`) is set, add `x-signature`: hex HMAC-SHA256 of the exact UTF-8 body string you send upstream.
2. Add `src/lib/nodedrSubmitEnv.ts` (or equivalent) that reads those env vars at runtime (no `NEXT_PUBLIC_` for secrets).
3. Add `src/lib/contactSubmitSchema.ts` shared between client and route; export the inferred type.
4. Wire the contact form(s) to `fetch("/api/submit", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ ...fields, gotcha }) })`, show inline success/error, never expose keys to the client.
5. Ensure CSP `connect-src` allows `'self'` for this fetch if the project uses CSP.
6. Document env vars in `.env.example` (public key name only as a placeholder; never commit real `sk_`).
Follow `f:/code/.cursor/rules/15-formsubmit-and-contact-forms.mdc` (Nodedr submit API section) and match file layout/naming to SeattleDrainCleaningCo unless this site's structure differs—then adapt minimally.
That gives a future session enough context to recreate the pattern without re-explaining it.In this monorepo the Next.js proxy is /api/contact-submit; POST /api/submit is the Go API (project keys). See /docs/contact-proxy for context.
Overview
Web app (this UI) runs in Next.js and talks to the API under /api/v1 (for example /api/v1/projects). Public form submissions use a separate route: POST /api/submit and do not use your account password — they use the project public key (pk_live_…) in the x-api-key header.
Typical flow: register → open Projects→ create or select a project → copy the public (and secret, for server-side HMAC) keys → point your form or fetch script at your site's /api/submit → review Submissions and Export as needed.
Architecture
- API (Go): authentication, projects, submissions, exports, optional update checks against GitHub.
- PostgreSQL: users, projects, submissions, system config (e.g. latest known app version).
- Object storage (external S3-compatible provider): optional presigned uploads for large files from the dashboard flow.
- Reverse proxy (nginx in the default stack): routes
/api/*to the API and everything else to the web UI. An exception proxies/api/contact-submitto Next.js for the optional marketing contact form (Nodedr upstream).
Quick start
- Create an account — Register with name, phone, email, and password (8+ characters). You are signed in with JWT access + refresh tokens stored in the browser.
- Open Projects — You get a default inbox; you can create more projects. Each has a unique public/secret pair.
- Copy the submit URL — Shown on the Projects page (same path on every host:
/api/submit). - Send a test POST — JSON body with at least a non-empty payload; header
x-api-key: <your public key>. - Open Submissions for that project to see rows.
Sign-in & tokens
Dashboard and API routes under /api/v1 require a Bearer access token (Authorization: Bearer <access_token>). Tokens are issued at POST /api/v1/auth/register and POST /api/v1/auth/login.
When the access token expires, the app refreshes it using POST /api/v1/auth/refresh with your refresh token. If refresh fails, sign in again.
Logout clears local storage and sends you to the home page so you can still read documentation and marketing content without being trapped in the app.
Projects & API keys
Each project is an inbox with its own keys:
- Public key (
pk_live_…) — Safe to embed in public frontends; used asx-api-keyforPOST /api/submit. - Secret key (
sk_live_…) — For HMAC signing on a server you trust; never expose in browser-only code. - Allowed origins (optional) — JSON array of exact origins (e.g.
https://example.com). If set, browser submissions may be restricted to those origins.
You can regenerate keys from the Projects page; old keys stop working immediately.
POST /api/submit
Public endpoint (no Bearer session). Identifies the destination project via the public key.
Headers
x-api-key— Required. Your project's public key (pk_live_…).x-signature— Optional. HMAC-SHA256 hex digest of the raw JSON body using the project secret, for server-side verification.Origin/Referer— Used with allowed origins when configured.
Body
JSON object. Common shape includes data for fields and files for references; the API stores the payload and may notify Telegram if configured.
{
"data": { "name": "Ada", "email": "ada@example.com" },
"files": []
}Limits — Body size is capped (configurable on the server). Each project has a submission cap (e.g. 5,000 rows); export or delete old data before hitting it.
CORS & origins
The API can allow browser Origin headers based on your deployment settings: explicit allowlists, same-host origins behind a reverse proxy, and optional relaxed rules for LAN or public submit. For embedded forms, CORS_PUBLIC_SUBMIT_ANY_ORIGIN may be enabled so any site can POST with a valid public key.
Use Allowed origins on the project when you want to restrict which frontends may submit for that key.
Rate limits
Sensitive public routes (login, register, etc.) and submit routes are rate-limited per IP and/or per key. If you hit a limit, wait briefly and retry. Self-hosted users can tune limits via environment variables (see below).
Dashboard
The Dashboard shows API health and recent submission activity. Use it as your operational overview after login.
Submissions & export
Per project, open Submissions to list rows. Use Export to download XLSX or PDF with a Bearer token. Bulk delete is available when you need to free space under the per-project cap.
Settings
Password — Change your account password from the Settings page.
API keys — Rotate account API key or all project keys if a key is exposed.
Storage credentials — Configure your external S3 endpoint, bucket, access key, and secret key for presigned uploads.
Self-hosting (Docker)
The default stack uses docker compose: API, web, PostgreSQL, S3-compatible storage, nginx on a port (e.g. 2512). Persist data via the documented volume paths. Set JWT_SECRET and database credentials in production.
Mount the project directory and Docker socket into the API container only if you want the dashboard "Update & restart" button to run compose on the host.
Environment variables (reference)
Names and defaults may vary by release; check your docker-compose.yml and the API config package for the source of truth. Common values:
| Variable | Purpose |
|---|---|
JWT_SECRET | Signing key for access/refresh tokens (change in production). |
DATABASE_URL | PostgreSQL connection string. |
ALLOWED_ORIGINS, CORS-related | Browser CORS for dashboard/API; tunnel-friendly options available. |
RATE_LIMIT_* | Tune RPM limits for public and authenticated routes. |
Troubleshooting
- 401 / invalid token on dashboard — Session expired or server secret changed; sign in again. Ensure the web app is rebuilt so token refresh runs.
- Cannot create project / SQL errors — Ensure API and migrations match; check API logs and DB connectivity.
- Submit rejected — Verify
x-api-key, public key format, allowed origins, and payload size.
