"Build forms, not form infrastructure. Store responses, not sessions."
Forms should be as simple as HTML but with the power of a database. Every form is version-controlled, every response is human-readable, and everything lives in text files you can grep.
Form builders are either: 1. SaaS nightmares: $29/month, vendor lock-in, data hostage situations 2. DIY headaches: Build database schemas, validation, admin interfaces, CSV exports 3. Serverless complexity: Lambda functions, API Gateway, DynamoDB, CloudFormation
Meanwhile, forms are fundamentally simple: collect data, store it, view it later.
dbbasic-forms combines the simplicity of static HTML forms with the power of TSV storage:
from dbbasic_forms import FormBuilder
# Create a form
builder = FormBuilder()
form = builder.create_form(
form_id="contact",
name="Contact Form",
fields=[
{"name": "email", "type": "email", "required": True},
{"name": "message", "type": "textarea"}
]
)
# Submit responses (from web handler)
form.submit_response({
"email": "user@example.com",
"message": "Love your product!"
})
# View responses
for response in form.get_responses():
print(response["email"], response["message"])
Your data lives in TSV files:
$ cat data/forms.tsv
id name description fields created_at
contact Contact Form Get in touch with us [{"name":"email"...}] 2024-01-15T10:30:00
$ cat data/responses_contact.tsv
id submitted_at email message
resp_20240115... 2024-01-15T14:22:00 user@example.com Love your product!
Forms collect structured data that needs: - Validation: Email format, required fields, length limits - Storage: Persistent, queryable, exportable - Management: Admin interface to view/export/delete - Notifications: Email alerts on submission
Traditional approaches: 1. Google Forms: Easy but data lives in Google 2. Typeform/Jotform: Beautiful but $$$ 3. Custom backend: Build everything from scratch
Forms map perfectly to tables: - Each form is a table schema (field definitions) - Each response is a row - Git provides versioning and deployment - Text files enable grep, awk, Excel
Trade-off: Not suitable for millions of responses per form. Use PostgreSQL if you expect >100K responses per form.
Considered: responses/contact/resp_001.json
Pros: One file per response, easy to add fields Cons: Can't query across responses, slow to list, hard to export CSV
TSV gives us SQL-like queries while staying human-readable.
forms.tsv)id name description fields settings created_at updated_at
contact Contact Get in touch [{"name":"email","type":...}] {"success_message":"..."} 2024-01-15T10:30:00 2024-01-15T10:30:00
Columns:
- id: Unique form identifier (slug-safe)
- name: Display name
- description: Subtitle/help text
- fields: JSON array of field definitions
- settings: JSON object (success message, button text, etc.)
- created_at: ISO 8601 timestamp
- updated_at: ISO 8601 timestamp
{
"name": "email",
"type": "email",
"label": "Email Address",
"placeholder": "you@example.com",
"help_text": "We'll never share your email",
"required": true,
"validation": "email"
}
Field Types:
- text: Single-line text input
- textarea: Multi-line text
- email: Email with validation
- number: Numeric input
- date: Date picker
- select: Dropdown menu
- radio: Radio buttons (single choice)
- checkbox: Checkboxes (multiple choice)
- file: File upload (stores path/URL)
responses_{form_id}.tsv)Dynamic schema based on form fields:
id submitted_at email message metadata
resp_20240115... 2024-01-15T14:22:00 user@example.com Love it! {"ip":"1.2.3.4"}
Standard Columns:
- id: Unique response ID (resp_YYYYMMDDHHMMSS{random})
- submitted_at: ISO 8601 timestamp
Dynamic Columns: One column per form field
Metadata Column: JSON object with:
- ip: Submitter IP address
- user_agent: Browser user agent
- referrer: Referrer URL
- Custom data passed by application
from dbbasic_forms import FormBuilder
builder = FormBuilder(data_dir="data")
Methods:
# Create a new form
form = builder.create_form(
form_id: str,
name: str,
description: str = "",
fields: List[Dict] = None,
settings: Dict = None
) -> Form
# Get existing form
form = builder.get_form(form_id: str) -> Optional[Form]
# List all forms
forms = builder.list_forms() -> List[Dict]
# Delete form and all responses
builder.delete_form(form_id: str) -> bool
from dbbasic_forms import FormBuilder
form = builder.get_form("contact")
Methods:
# Submit a response
response_id = form.submit_response(
response_data: Dict[str, Any]
) -> str
# Get responses (paginated)
responses = form.get_responses(
limit: int = None,
offset: int = 0
) -> List[Dict]
# Count responses
count = form.count_responses() -> int
# Get single response
response = form.get_response(response_id: str) -> Optional[Dict]
# Delete response
form.delete_response(response_id: str) -> bool
# Update form configuration
form.update_form(
name: str = None,
description: str = None,
fields: List[Dict] = None,
settings: Dict = None
) -> bool
# Export to CSV
csv_string = form.export_responses_csv() -> str
# Get form data
data = form.to_dict() -> Dict
The core implementation is ~200 lines of Python:
class FormBuilder:
def __init__(self, data_dir="data"):
self.forms = TSV("forms",
["id", "name", "description", "fields", "settings",
"created_at", "updated_at"],
data_dir=data_dir, indexes=["id"])
def create_form(self, form_id, name, description="",
fields=None, settings=None):
now = datetime.utcnow().isoformat()
self.forms.insert({
"id": form_id,
"name": name,
"description": description,
"fields": json.dumps(fields or []),
"settings": json.dumps(settings or {}),
"created_at": now,
"updated_at": now
})
return Form(form_id, self.data_dir)
class Form:
def __init__(self, form_id, data_dir):
# Load form definition
# Create responses_{form_id} TSV with dynamic columns
def submit_response(self, response_data):
response_id = f"resp_{datetime.utcnow():%Y%m%d%H%M%S%f}"
self.responses.insert({
"id": response_id,
"submitted_at": datetime.utcnow().isoformat(),
**response_data
})
return response_id
from dbbasic_forms import FormBuilder
builder = FormBuilder()
contact_form = builder.create_form(
form_id="contact",
name="Contact Form",
description="Get in touch with us",
fields=[
{
"name": "name",
"type": "text",
"label": "Name",
"required": True
},
{
"name": "email",
"type": "email",
"label": "Email",
"required": True,
"validation": "email"
},
{
"name": "message",
"type": "textarea",
"label": "Message",
"placeholder": "What's on your mind?"
}
],
settings={
"success_message": "Thanks! We'll get back to you soon.",
"button_text": "Send Message"
}
)
# Flask example
from flask import Flask, request, jsonify
from dbbasic_forms import FormBuilder
app = Flask(__name__)
builder = FormBuilder()
@app.route('/api/forms/<form_id>/submit', methods=['POST'])
def submit_form(form_id):
form = builder.get_form(form_id)
if not form:
return jsonify({"error": "Form not found"}), 404
# Submit response
response_id = form.submit_response(request.json)
# Optional: Send notification email
# send_email(admin_email, f"New form submission: {response_id}")
return jsonify({
"success": True,
"response_id": response_id,
"message": form.settings["success_message"]
})
from dbbasic_forms import FormBuilder
builder = FormBuilder()
form = builder.get_form("contact")
# Get recent responses
responses = form.get_responses(limit=10)
for response in responses:
print(f"{response['submitted_at']}: {response['email']}")
print(f" {response['message']}")
form = builder.get_form("contact")
csv_data = form.export_responses_csv()
# Save to file
with open("contact_responses.csv", "w") as f:
f.write(csv_data)
# Or return via HTTP
from flask import Response
return Response(csv_data, mimetype="text/csv")
dbbasic-forms integrates with dbbasic-admin for visual management.
# dbbasic_forms/admin.py
ADMIN_CONFIG = [
{
'icon': '📋',
'label': 'Forms',
'href': '/admin/forms',
'order': 20,
}
]
GET /admin/forms - List all forms with statsGET /admin/forms/create - Form builder interfacePOST /admin/forms/create - Create new formGET /admin/forms/{id}/edit - Edit formPOST /admin/forms/{id}/edit - Update formGET /admin/forms/{id}/responses - View responsesPOST /admin/forms/{id}/delete - Delete form# dbbasic_forms/api/forms/list.py
from dbbasic_forms import FormBuilder
def GET(request):
builder = FormBuilder()
forms = builder.list_forms()
# Render template
return render_template("forms/list.html", forms=forms)
✅ Form Builder: Visual drag-drop interface ✅ Field Types: Text, email, textarea, select, radio, checkbox, date, file ✅ Validation: Required fields, email format, custom patterns ✅ Response Management: View, export, delete ✅ CSV Export: One-click export to spreadsheet ✅ Git-Friendly: All data in TSV files
🚧 Conditional Logic: Show/hide fields based on answers (coming soon) 🚧 File Uploads: Store files with responses (coming soon) ✅ Email Notifications: Alert on new submissions (via dbbasic-email) 🚧 Spam Protection: reCAPTCHA integration (coming soon) 🚧 Multi-page Forms: Split long forms into steps (coming soon) 🚧 Analytics: Response rates, completion time (coming soon)
Benchmarks on M1 MacBook Pro:
Limits: - Forms per project: Unlimited - Responses per form: 100K recommended, 1M maximum - Fields per form: 50 recommended - File upload size: 10MB default (configurable)
pip install dbbasic-forms
# Data is stored in ./data directory
# Commit to git: git add data/ && git commit -m "Add form data"
# On your server
git clone https://github.com/yourorg/yourapp.git
cd yourapp
pip install -r requirements.txt
# Data is in the repo
# New responses append to TSV files
# Push updates: git add data/ && git commit && git push
For high-traffic forms (>1K responses/day):
# Use PostgreSQL for responses, TSV for form definitions
from dbbasic_forms import FormBuilder
builder = FormBuilder(
data_dir="data", # Form definitions in git
response_backend="postgresql://..." # Responses in DB
)
pip install dbbasic-forms[dev]
pytest tests/
Example test:
def test_form_creation():
builder = FormBuilder(data_dir="test_data")
form = builder.create_form(
"test", "Test Form",
fields=[{"name": "email", "type": "email"}]
)
assert form.count_responses() == 0
form.submit_response({"email": "test@example.com"})
assert form.count_responses() == 1
# Export Google Form responses as CSV
# Import into dbbasic-forms
import csv
from dbbasic_forms import FormBuilder
builder = FormBuilder()
form = builder.create_form("imported", "Imported Form", fields=[...])
with open("google_form_export.csv") as f:
reader = csv.DictReader(f)
for row in reader:
form.submit_response(row)
Use their API to export data, then import:
import requests
from dbbasic_forms import FormBuilder
# Fetch from Typeform API
response = requests.get("https://api.typeform.com/forms/{id}/responses")
data = response.json()
builder = FormBuilder()
form = builder.create_form("imported", "Imported Form", fields=[...])
for item in data["items"]:
form.submit_response(item["answers"])
All fields are validated before storage:
# Email validation
if field["type"] == "email":
if not re.match(r"[^@]+@[^@]+\.[^@]+", value):
raise ValidationError("Invalid email")
# Required fields
if field.get("required") and not value:
raise ValidationError("Field is required")
All output is HTML-escaped in admin interface.
# Only allow specific file types
ALLOWED_EXTENSIONS = {".jpg", ".png", ".pdf", ".doc", ".docx"}
# Sanitize filenames
import uuid
filename = f"{uuid.uuid4()}{ext}"
Recommended for public forms:
from flask_limiter import Limiter
limiter = Limiter(app, key_func=lambda: request.remote_addr)
@app.route('/api/forms/<form_id>/submit', methods=['POST'])
@limiter.limit("5 per minute")
def submit_form(form_id):
# ...
| Feature | dbbasic-forms | Google Forms | Typeform | Custom Build |
|---|---|---|---|---|
| Cost | Free | Free/Limited | $25+/mo | Dev time |
| Data Ownership | ✅ | ❌ | ❌ | ✅ |
| Git-Friendly | ✅ | ❌ | ❌ | Maybe |
| Grep Responses | ✅ | ❌ | ❌ | ❌ |
| Setup Time | 1 min | 1 min | 5 min | Hours |
| Self-Hosted | ✅ | ❌ | ❌ | ✅ |
| Visual Builder | ✅ | ✅ | ✅ | Build it |
| Conditional Logic | 🚧 | ✅ | ✅ | Build it |
| File Uploads | 🚧 | ✅ | ✅ | Build it |
| Email Alerts | Via add-on | ✅ | ✅ | Build it |
❌ High-volume transactional forms: Use PostgreSQL + dedicated form service ❌ Payment forms: Use Stripe Checkout or similar ❌ Multi-tenant SaaS: Need database-per-tenant isolation ❌ Real-time collaboration: Multiple people editing same form ❌ Complex workflows: Approval chains, routing, integrations
Rule of thumb: If your form responses can't safely live in Git (because they're user transactions), use a database.
dbbasic-forms proves that form builders don't need to be SaaS. By storing everything in TSV files, you get:
grep alice data/responses_contact.tsvPerfect for: - Content sites: Contact forms, newsletters, surveys - Prototypes: Get feedback fast without SaaS overhead - Internal tools: HR forms, IT requests, feedback - Static sites: Add forms to Jekyll/Hugo/Gatsby
Remember: The best form builder is the one you control.