← Back to Modules

dbbasic-forms: Git-Native Form Builder Specification

Philosophy

"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.

The Problem

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.

The Solution

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!

Architecture Decision History

Why Forms Need Databases

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

Why TSV Storage Works

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.

Why Not JSON Files?

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.

Technical Specification

Storage Format

Form Definitions (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

Field Definition Schema

{
  "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)

Response Storage (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

API

FormBuilder Class

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

Form Class

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

Implementation

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

Usage Examples

Creating a Contact Form

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"
    }
)

Handling Form Submissions (Web Framework)

# 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"]
    })

Viewing Responses

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']}")

Exporting to CSV

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")

Admin Interface

dbbasic-forms integrates with dbbasic-admin for visual management.

Auto-Discovery

# dbbasic_forms/admin.py
ADMIN_CONFIG = [
    {
        'icon': '📋',
        'label': 'Forms',
        'href': '/admin/forms',
        'order': 20,
    }
]

Admin Routes

Example Admin Handler

# 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)

Features

Core Features

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

Advanced Features

🚧 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)

Performance

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)

Deployment

Development

pip install dbbasic-forms

# Data is stored in ./data directory
# Commit to git: git add data/ && git commit -m "Add form data"

Production (Git-Based)

# 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

Production (Database-Backed)

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
)

Testing

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

Migration

From Google Forms

# 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)

From Typeform/Jotform

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"])

Security

Input Validation

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")

XSS Prevention

All output is HTML-escaped in admin interface.

File Upload Security

# Only allow specific file types
ALLOWED_EXTENSIONS = {".jpg", ".png", ".pdf", ".doc", ".docx"}

# Sanitize filenames
import uuid
filename = f"{uuid.uuid4()}{ext}"

Rate Limiting

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):
    # ...

Comparison

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

When NOT to Use

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.

Future Roadmap

Conclusion

dbbasic-forms proves that form builders don't need to be SaaS. By storing everything in TSV files, you get:

Perfect 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.