Docs

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.

This page is a condensed quick reference. The full README, deployment guide, and API contract live in the GitHub repository.

Requirements

Install

One command clones the repo, generates strong random secrets on first run, and brings up the full stack. Requires Docker Engine and the Compose v2 plugin. If you installed Docker from Debian's docker.io package, see the Debian prerequisite steps in the docs before running.

One-line installer (requires git + Docker Compose v2)

curl -fsSL https://raw.githubusercontent.com/Raktim94/Submify/main/install.sh | bash

Requires Docker Engine and the Compose v2 plugin. Once the containers are up, open http://localhost:2512 and create your first account at /register.

Configuration

No .env is required to get started — compose-up.sh / Compose-Up.ps1 auto-create .env.auto with strong random POSTGRES_PASSWORD and JWT_SECRET values on first run. Copy .env.example to .env only to override defaults (custom CORS origins, port, cookie settings, and so on).

  • Single published port — Nginx is the only exposed port (default 2512), proxying /api/* to the Go API and everything else to the dashboard.
  • One account per instance — registration only works while no account exists yet (GET /api/v1/system/bootstrap-status). After the first account is created, sign-up closes and POST /api/v1/auth/register returns 403.
  • Optional storage — connect any S3-compatible bucket per project from the dashboard for presigned uploads. No storage container ships with the stack.

API overview

Dashboard and API routes live under /api/v1 and require a Bearer access token. Public form submissions use a separate route, POST /api/submit, authenticated with a project's public key in the x-api-key header — not your account password.

The full request/response contract for every endpoint (auth, projects, submissions, exports, uploads) is documented in docs/api.md on GitHub.

Client portal (share view-only access)

Each project has its own password-protected portal at https://<your-host>/<slug> (for example /acme-contact). Share the URL and password with a client and they can view and export that project's submissions — and nothing else. No dashboard account, no API keys, no delete rights, and no visibility into your other projects.

  • Auto-generated password — creating a project mints a strong random portal password, shown once so you can copy and share it. Only its Argon2id hash is stored; regenerate a fresh one any time.
  • You stay in control — rename the slug, rotate or clear the password, and enable or disable the portal per project from the dashboard.
  • Secure by default — sessions are project-scoped in an HttpOnly cookie, login is rate-limited per project, and every portal response is sent with Cache-Control: no-store.

Wire up a form with an AI assistant

Paste the prompt below into your AI coding assistant (Cursor, Claude, or similar) to scaffold a form that posts to your Submify instance. Replace <MY-SUBMIFY-HOST> and <PROJECT_PUBLIC_KEY> with your own values first.

Paste into Cursor, Claude, or any coding assistant

You are helping me connect an HTML/JS contact form to my self-hosted Submify form backend.

Submify facts:
- Submit endpoint: POST https://<MY-SUBMIFY-HOST>/api/submit
- Auth header: "x-api-key: <PROJECT_PUBLIC_KEY>" (looks like pk_live_...). This key is safe to expose in the browser.
- Request body is JSON: { "data": { ...your form fields... }, "files": [] }
- Success is HTTP 201. On error, the JSON response has an "error" string.
- The project secret key (sk_live_...) is NEVER put in browser code. Use it only server-side to sign the raw body with HMAC-SHA256 and send it in the "x-signature" header.

Please:
1. Build an accessible contact form (name, email, message) with client-side validation and a hidden honeypot field named "gotcha". If the honeypot is filled, silently succeed without sending (it is a bot).
2. Submit with fetch() and AbortSignal.timeout(18000). Show inline success and error states, and disable the button while sending.
3. Prefer posting to a same-origin server route (e.g. a Next.js route handler) that forwards to the Submify endpoint, so the public key stays out of the client bundle and you can add the optional x-signature HMAC. If a server route is not possible, post directly from the browser with only the public key.
4. Read the Submify host and public key from environment variables — do not hard-code them.

After submissions arrive I review and export them in the Submify dashboard, or share a read-only client portal link (https://<MY-SUBMIFY-HOST>/<project-slug>) and its password with the client so they can view and export their own submissions.

Accessing from another device (LAN / network IP)

By default Nginx binds to 127.0.0.1, so the dashboard and API are reachable only from the same machine. To access Submify from another device on your network — or from the internet — follow these three steps.

1. Add these two lines to your .env file (create it from .env.example if it does not exist yet):

# Bind Nginx to all interfaces so other devices can reach port 2512.
# Replace 0.0.0.0 with a specific IP to restrict to one interface.
SUBMIFY_BIND_IP=0.0.0.0

# Add your server IP or hostname to the CORS allowlist.
# Replace 192.168.1.100 with your actual server IP or domain.
ALLOWED_ORIGINS=http://localhost:2512,http://127.0.0.1:2512,http://192.168.1.100:2512

2. Restart the stack:

docker compose up -d

3. Open port 2512 on your host firewall:

# UFW (Ubuntu / Debian)
sudo ufw allow 2512/tcp

# firewalld (Fedora / RHEL / CentOS)
sudo firewall-cmd --add-port=2512/tcp --permanent && sudo firewall-cmd --reload

# iptables
sudo iptables -I INPUT -p tcp --dport 2512 -j ACCEPT

Then open http://<server-ip>:2512 from any device on the same network. For internet-facing deployments, put Submify behind a reverse proxy (Nginx, Caddy, Traefik) or a Cloudflare Tunnel with TLS rather than exposing port 2512 directly.

Troubleshooting

Debian: Docker Compose v2 not found

If the installer fails with Docker Compose v2 plugin is required, you likely installed Docker from Debian's repositories (apt install docker.io) rather than Docker's official source. The Compose v2 plugin is not included in Debian's Docker package. Run these commands before retrying the installer:

1. Add Docker's official repository:

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list

sudo apt update

2. Remove the conflicting Debian buildx package:

sudo apt remove docker-buildx

3. Install the official plugins:

sudo apt install docker-buildx-plugin docker-compose-plugin

If apt install reports broken packages, run sudo apt --fix-broken install first, then retry step 3.

4. Verify and re-run the installer:

docker compose version
curl -fsSL https://raw.githubusercontent.com/Raktim94/Submify/main/install.sh | bash

Other issues

  • API exits with a JWT_SECRET error — with GIN_MODE=release, the secret must be at least 32 characters. Set it in .env, or use ./scripts/compose-up.sh so .env.auto supplies one.
  • Nothing on the published port — check your firewall, docker compose ps, and the Nginx container logs.
  • Build fails on a small VPS — re-run with --progress=plain to read the full error; try --parallel 1 or add swap if the build runs out of memory.