Yet another form handler. FastCGI Form Collector which turns submissions into to-do items for manual completion. Form fields are arbitrary and stored as submitted in JSON format. This project provides a minimal FastCGI listener for HTML form submissions, backed by PostgreSQL storage and an authenticated admin dashboard served over FastCGI.
- FastCGI listener on a Unix domain socket for dynamic form submissions and the admin dashboard (served via the same socket),
with an optional TCP FastCGI listener via
-tcp <port>. - PostgreSQL persistence with JSONB storage for arbitrary form fields and request metadata (IP, user agent, referrer, timestamp).
- Admin dashboard with login (bcrypt passwords stored in the database), pagination, status management (
new,in_progress,complete,archived), CSRF protection, hardened cookies, and nicely formatted JSON payloads. - Built-in throttling that blocks abusive IPs (over 4 submissions per minute) for 24 hours and temporarily pauses all submissions for 5 minutes when a burst of distinct IPs appears.
- Request caps to protect the FastCGI endpoint from floods (64KB body and 200-field limit, with an adjustable upload budget on top).
- Optional file capture that, when enabled, stores an uploaded file inside the
_ftdchroot with a unique timestamped name and records both the stored path and the original filename alongside the submission JSON. File uploads are disabled by default. - Sample submitter forms:
sample_form.html(no file upload) andsample_form_upload.html(includes a single file field) that post to the FastCGI endpoint.
Set the following environment variables before running the server:
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string (e.g., postgres://user:pass@localhost:5432/forms). |
Required |
FASTCGI_SOCKET |
Unix socket path for the FastCGI listener. | /var/www/run/ftd.sock |
FORM_PATH |
FastCGI path for submissions. | /form |
ADMIN_PREFIX |
FastCGI path prefix for the admin dashboard (login, dashboard, static). | /form/admin |
SESSION_SECRET |
Secret used to sign admin session cookies; if omitted, an ephemeral random key is generated (sessions reset on restart). | Generated per process when unset |
SESSION_COOKIE_INSECURE |
If set, disables the default Secure cookie flag for admin/csrf cookies (useful for plain HTTP dev). |
Not set (Secure cookies enabled) |
MAX_UPLOAD_MB |
Maximum allowed file upload size in megabytes; 0 disables uploads. |
0 |
Initialize the schema before the first run (ftd does not apply migrations at runtime and will exit if required tables are missi ng):
psql "$DATABASE_URL" -f schema.sqlThe schema creates submissions (with metadata, optional stored file path, reviewer comment, and JSONB payload), admin_users (bcrypt password hashes), and submission_blocks (temporary throttling windows). Indexes are added for status filtering and date ordering to keep pagination fast.
The schema also seeds a default admin account (admin / change-me); the dashboard will display a red reminder until you change it via the password form.
If you prefer to rotate the password directly from psql instead of using the dashboard, run these single-line commands at the psql> prompt to enable pgcrypto and set a new bcrypt hash for the admin account:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
UPDATE admin_users SET password_hash = crypt('your-new-password', gen_salt('bf')) WHERE username = 'admin';
- Export the required environment variables (see above).
- Start the service:
go run . - Point your FastCGI-capable web server at the configured socket (default
/var/www/run/ftd.sock) for both form and admin paths. The service defaults to/formfor submissions and/form/adminfor the dashboard, configurable viaFORM_PATHandADMIN_PREFIXenv vars. Alternatively, start the service with-tcp 9000(or another port) and configure your front-end to FastCGI proxy to127.0.0.1:9000. - Serve
sample_form.htmlvia your web server (or open from disk) and point itsactionat/form(or yourFORM_PATH) on your FastCGI front-end. Access the admin dashboard through the same front-end at/form/admin/(or yourADMIN_PREFIX). To redirect submitters to a thank-you page after a successful submission, include a hidden field namedredirectwith an absolute or relative HTTP(S) URL; the handler issues a 303 See Other to that target once the form is stored.
Note: Building requires downloading
github.com/lib/pqandgolang.org/x/crypto. Ensure outbound module downloads are permitted by your environment.
- Uploads are disabled by default. Set
MAX_UPLOAD_MBto a positive integer to allow a single file upload per submission, capped to that size and counted against the FastCGI body budget. - Uploaded files are stored under
uploads/inside the_ftdchroot (created on demand) using names likeftd.20240101T000000Z,89abcd12. The stored path lives in thefile_pathcolumn, and both the stored name and any client-supplied original name are injected into theform_dataJSON as_upload_stored_filenameand_upload_original_filename. When a write fails, the row keepsFailed Upload (<status code>)infile_pathand in_upload_stored_filenameso reviewers can see the error. - The lightweight sample
sample_form.htmlremains text-only; usesample_form_upload.htmlfor an upload-capable example (withenctype="multipart/form-data").
Rows start as new. The admin UI lets you move them to in_progress, complete, or archived. Completed submissions remain available but collapse into the lower section; archived items stay out of the main dashboard and live in the dedicated Archived view with its own pagination. A bulk "Archive completed" control is available on the Active dashboard to sweep all completed rows into the archived view at once. Each submission also supports an internal reviewer comment field; it can be edited alongside status updates and is rendered in a muted, read-only state when the submission is archived.
- Each client IP may submit at most 4 forms per rolling minute. Exceeding that threshold blocks the IP for 24 hours and returns HTTP 429 with a
Retry-Afterhint. - If a sudden burst of 30 or more distinct IPs arrives within a minute, the service pauses all submissions for 5 minutes to mitigate abuse and returns HTTP 503 with
Retry-After. - Block information is stored in the
submission_blockstable; expired blocks are cleaned when new requests arrive.
main.go– FastCGI listener, admin routes, and handlers.schema.sql– PostgreSQL schema and indexes.templates/– Admin HTML templates.static/– Admin CSS assets.sample_form.html– Example HTML form posting to the FastCGI endpoint.rc.d/ftd– OpenBSDrc.dhelper that loads/etc/ftd.envand backgrounds the daemon underrcctl.
- Set a strong
SESSION_SECRETbefore first run; if you omit it, the server will generate a random per-process key and all sessions will be invalidated on restart. The initial admin account (admin) ships in the schema with passwordchange-me—the dashboard surfaces a warning until you change it via the built-in password form. - Admin sessions are signed (with refresh-on-activity to avoid logging out active tabs) and constrained with
Secure,HttpOnly, andSameSite=Strictflags by default. SetSESSION_COOKIE_INSECURE=1only for non-TLS local testing. - CSRF tokens are required on admin POSTs (login and status updates) and validated against secure cookies.
- Admin responses set conservative security headers (CSP, frame-ancestors deny, referrer/permissions policies, cache disabling, MIME sniff protection) to reduce injection and clickjacking risk.
- Submission bodies are capped (64KB) and oversized/overlong forms are rejected to slow data flooding.
- Terminate TLS at your front-end web server (nginx/httpd) and forward
X-Forwarded-Forso the app can capture real client IPs. - Restrict filesystem permissions on the FastCGI socket (
FASTCGI_SOCKET) so only the web server can connect. The socket is created before chroot/drop-privilege when starting asroot. - If the process starts as
root, it will chroot to the_ftduser's home and drop privileges to that account after opening the PostgreSQL socket and FastCGI listener. Create the_ftduser and ensure its home directory exists before launching. - On OpenBSD, pledge(2) is used: startup allows file/socket setup and DNS, then pledges are tightened after connecting to PostgreSQL and preparing listener sockets (with promises adjusted depending on whether a Unix socket or TCP FastCGI port is used).
- Install dependencies and create the service account:
pkg_add go postgresql-client useradd -m _ftd
- Initialize the database schema and admin user (replace credentials as needed):
Other environment variables use the defaults noted in the Configuration table; override them if you need a custom socket path or URL prefixes.
createdb ftd psql ftd -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" psql ftd < schema.sql env \ DATABASE_URL="postgres://<user>:<pass>@<host>:<port>/<db>" \ go build -o /usr/local/bin/ftd
- Create the run directory and permissions for httpd:
install -d -m 750 -o _ftd -g www /var/www/run
- Configure
/etc/httpd.conf:server "example.com" { listen on * port 80 location "/form" { fastcgi socket "/var/www/run/ftd.sock" } location "/form/admin/*" { fastcgi socket "/var/www/run/ftd.sock" } } - Templates and admin static assets are embedded in the binary and served from the
_ftdchroot (the_ftduser home). You do not need to copy thetemplates/orstatic/directories to the filesystem; only place the sample HTML forms in your web root if you want to expose them directly. - Install the provided
rc.dhelper and supply environment via/etc/ftd.env:install -m 755 rc.d/ftd /etc/rc.d/ftd cat <<'EOF' > /etc/ftd.env
DATABASE_URL="postgres://:@:/"
EOF
7. Enable and start the services (the daemon opens sockets before chrooting/dropping to `_ftd`):
```sh
rcctl enable httpd
rcctl start httpd
rcctl enable ftd
rcctl start ftd
```
8. Log into the dashboard at `/form/admin/` with `admin` / `change-me`, then update the password using the on-page form (a warning remains until you do).
### Linux + nginx (FastCGI over Unix socket)
1. Install dependencies and create the service account:
```sh
sudo apt-get update && sudo apt-get install -y golang postgresql-client nginx
sudo useradd -m -s /usr/sbin/nologin _ftd
- Initialize the database schema and admin user:
All other environment variables keep their documented defaults unless you override them (e.g., socket path or URL prefixes).
createdb ftd psql ftd -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" psql ftd < schema.sql env \ DATABASE_URL="postgres://<user>:<pass>@<host>:<port>/<db>" \ go build -o /usr/local/bin/ftd
- Prepare the FastCGI socket path for nginx:
sudo install -d -m 750 -o _ftd -g www-data /var/www/run
- Configure nginx (e.g.,
/etc/nginx/sites-available/ftd.conf):Enable the site and reload nginx:server { listen 80; server_name example.com; location /form { include fastcgi_params; fastcgi_pass unix:/var/www/run/ftd.sock; } location /form/admin/ { include fastcgi_params; fastcgi_pass unix:/var/www/run/ftd.sock; } }sudo ln -s /etc/nginx/sites-available/ftd.conf /etc/nginx/sites-enabled/ftd.conf sudo nginx -t sudo systemctl reload nginx
- Templates and admin static assets are embedded in the binary and served from the
_ftdchroot (the_ftduser home). There is no need to copytemplates/orstatic/onto the host filesystem; only publish the sample HTML forms if you wish to serve them directly. - Run the FastCGI service (with socket creation before chroot/drop-privilege):
sudo -u _ftd /usr/local/bin/ftd
- Sign in at
/form/admin/asadmin/change-meand rotate the password via the dashboard form; the UI warns while the default remains.