A self-hosted drop-in backend for HTML forms. Accept, store, review, and route form submissions without surrendering control to third-party SaaS providers.
π formlander.com
Formlander enables developers running static or serverless sites to handle form submissions with a single-binary deployment. Store submissions in SQLite, review them in a lightweight admin UI, and route data asynchronously to webhooks or email via Mailgun.
- Single-binary deployment β One executable with SQLite storage, no external dependencies
- Admin dashboard β View and manage forms, submissions, and delivery status
- Asynchronous delivery β Queue webhook and email notifications with retry logic
- Spam protection β Configurable honeypot fields and rate limiting
- API-first design β Dashboard consumes the same REST endpoints available for integrations
- Privacy-focused β All data stored locally; optional Mailgun integration for email forwarding
For improved reliability, include the optional SDK that adds automatic retry with exponential backoff:
<script src="https://your-formlander.com/assets/formlander.js"></script>
<form action="https://your-formlander.com/forms/contact/submit?token=YOUR_TOKEN" method="post">
<input name="email" type="email" required>
<button type="submit">Send</button>
</form>The SDK auto-detects Formlander forms and enhances them with:
- Retry logic β 3 attempts with exponential backoff on 503/network errors
- Graceful degradation β Falls back to normal form POST if JS fails
- Loading states β Disables form and shows "Sending..." during submission
Forms work without the SDK via standard HTML POST. The SDK is purely an enhancement.
Install Formlander with Docker, Caddy reverse proxy, and automatic SSL certificates:
curl -fsSL https://raw.githubusercontent.com/karloscodes/formlander/master/install.sh | sudo bashThis interactive installer will:
- Check system requirements (Docker, ports 80/443)
- Prompt for your domain name
- Set up Caddy as a reverse proxy with automatic HTTPS
- Configure automatic daily backups
- Start the Formlander container
After installation, access your dashboard at https://your-domain.com
Management commands:
formlander update # Update to latest version
formlander reload # Reload containers
formlander restore-db # Restore from backup
formlander change-admin-password # Reset admin password
formlander upgrade-to-pro # Upgrade to Pro versionFirst, generate and save your session secret:
# Generate once and save this value securely
export FORMLANDER_SESSION_SECRET=$(openssl rand -hex 32)
echo "Save this secret: $FORMLANDER_SESSION_SECRET"Then run the container with your saved secret:
docker run -d \
-p 8080:8080 \
-e FORMLANDER_SESSION_SECRET="your-saved-secret-here" \
-v $(pwd)/storage:/app/storage \
karloscodes/formlander:latestImportant: Use the same FORMLANDER_SESSION_SECRET value across restarts to prevent logging out all users.
Access the admin dashboard at http://localhost:8080 with default credentials:
- Email:
admin@formlander.local - Password:
formlander(you'll be prompted to change this on first login)
- Download the latest release from the Releases page
- Generate and save your session secret:
# Generate once and save this value export FORMLANDER_SESSION_SECRET=$(openssl rand -hex 32) echo "Save this secret: $FORMLANDER_SESSION_SECRET" export FORMLANDER_DATA_DIR=./storage
- Run the binary:
./formlander
Formlander uses Viper for flexible configuration. You can configure via:
- Environment variables (prefix:
FORMLANDER_) .envfile for easier local development- Environment variables always override
.envfile values
Required Environment Variable (Production Only):
FORMLANDER_SESSION_SECRET- HMAC secret for signing session cookies (fixed default in dev/test)
Optional Environment Variables:
FORMLANDER_ENV- Environment mode:development,production(default:production)FORMLANDER_PORT- HTTP port (default:8080)FORMLANDER_LOG_LEVEL- Log level:debug,info,warn,error(default:error)FORMLANDER_DATA_DIR- Data directory path (default:./storage)
Note: In development/test, a fixed default secret is used if not set, allowing sessions to persist across restarts.
Or use a .env file (.env):
FORMLANDER_ENV=production
FORMLANDER_PORT=8080
FORMLANDER_SESSION_SECRET=your-secret-here
FORMLANDER_LOG_LEVEL=info
FORMLANDER_DATA_DIR=./storage- Clone the repository
- Build:
make build
- Generate and save your session secret, then run:
# Generate once and save this value export FORMLANDER_SESSION_SECRET=$(openssl rand -hex 32) echo "Save this secret: $FORMLANDER_SESSION_SECRET" ./bin/formlander
Formlander uses semantic versioning. Docker images are published via GitHub Releases when version tags are pushed.
# Latest stable release
docker pull karloscodes/formlander:latest
# Specific version
docker pull karloscodes/formlander:v1.0.0
# Major version (receives minor + patch updates)
docker pull karloscodes/formlander:v1Note: Docker images are published automatically via GitHub Actions when a version tag (e.g., v1.0.0) is pushed to the repository.
If you prefer to run a native binary instead of Docker:
git clone https://github.com/karloscodes/formlander.git
cd formlander
make build
export FORMLANDER_SESSION_SECRET=$(openssl rand -hex 32)
./bin/formlanderSupported platforms for building from source:
- Linux (amd64, arm64)
- macOS (amd64, arm64)
Formlander follows a Phoenix Context Architecture, organizing code into bounded contexts with clear separation of concerns:
[Static Site] --> POST /forms/:slug/submit
|
[Formlander]
/ | \
[SQLite] [Jobs] [Admin UI]
|
[Webhook/Email Dispatchers]
|
[External Services/Mailgun]
- HTTP Server β Fiber-based cartridge wrapper handling public submissions and admin dashboard
- Database Layer β GORM + SQLite with WAL mode
- Custom Write Retry Logic β
dbtxn.WithRetryensures writes eventually succeed despite SQLite's single-writer constraint - Job System β In-process dispatchers for asynchronous webhook and email delivery
- Cartridge Context β Request-scoped dependency injection providing type-safe access to logger, config, and database
Due to SQLite's single-writer limitation, all write operations use a custom retry mechanism (internal/pkg/dbtxn/retry.go) that:
- Detects busy/locked database errors
- Retries with exponential backoff (up to 10 attempts)
- Adds jitter to prevent thundering herd issues
- Works alongside WAL mode, busy_timeout pragmas, and immediate transaction locks
This ensures writes eventually succeed even under concurrent load.
Build and run locally:
make build
make devRun tests:
make testContributions are welcome! Please open an issue first to discuss proposed changes, or submit a pull request for bug fixes and improvements.
Formlander License Agreement β see LICENSE file for details.