• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
matthewmateo

matthewmateo

forms

Public
Like
forms
Home
Code
11
backend
3
frontend
5
shared
1
.vtignore
AGENTS.md
FormSubmission.svelte
README.md
config.json
deno.json
example-usage.svelte
main.ts
Branches
1
Pull requests
Remixes
History
Environment variables
2
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
README.md
Code
/
README.md
Search
…
README.md

Form Service

A universal form submission service for all your websites. No more paying for form services!

Setup

1. Get a Resend API Key

  1. Sign up at resend.com (free tier available)
  2. Create an API key

2. Set Environment Variables in Val Town

In your Val Town project settings, add these environment variables:

  • RESEND_API_KEY - Your Resend API key (required)
  • OWNER_EMAIL - Your email address where notifications will be sent (required)
  • FROM_EMAIL - Sender email address (optional, defaults to onboarding@resend.dev)

3. Deploy

Set the HTTP trigger on backend/index.ts and you're good to go!

4. Configure Email Recipients and Auto-responders (Optional)

Edit config.json to route form submissions and set default auto-responders per domain/page:

{ "example.com/contact": { "recipient": "contact@example.com", "autorespond": { "message": "Thanks for contacting us! We'll get back to you within 24 hours.", "from": "noreply@example.com" } }, "client1.com/": { "recipient": "client1@example.com", "autorespond": { "message": "Thank you for your message!", "from": "hello@client1.com" } }, "mysite.com/careers": { "recipient": "hr@mysite.com", "autorespond": "Thanks for your interest! We'll review your application soon." } }

Note: autorespond can be either a string (uses default FROM_EMAIL) or an object with message and from fields.

Portal Token: Each domain gets a unique portal_token (e.g., "client-abc123"). This creates a client portal at /portal/client-abc123 where your client can:

  • View all their form submissions
  • See current settings (recipient, autoresponder)
  • (Coming soon) Update settings themselves

How it works:

  • Exact match first: domain.com/page β†’ specific recipient and autorespond
  • Falls back to domain: domain.com β†’ domain recipient and autorespond
  • Falls back to OWNER_EMAIL if no match
  • Forms can override autorespond with _autorespond field

Client Portals

Each domain in your config.json gets a unique portal URL that you can share with clients:

https://yourval.web.val.run/portal/sfl-abc123

Clients can:

  • βœ… View all submissions to their site
  • βœ… See auto-updated submission count
  • ⏳ Update recipient emails (coming soon)
  • ⏳ Customize auto-responder message (coming soon)

Security Note: Portal URLs use random tokens. Keep them private - anyone with the URL can view submissions.

5. (Optional) Custom Domain

If you want emails to come from your own domain instead of onboarding@resend.dev:

  1. Verify your domain in Resend
  2. Set FROM_EMAIL environment variable to noreply@yourdomain.com

Features

  • βœ… One endpoint for all your sites
  • βœ… Auto-organizes by domain and page
  • βœ… Email notifications on new submissions
  • βœ… Error notifications sent to owner
  • βœ… Custom auto-responder per form
  • βœ… Admin dashboard to view submissions
  • βœ… Spam protection (honeypot + rate limiting)
  • βœ… Dynamic field support
  • βœ… Per-domain email routing via config file

Usage

Basic Form Submission

<form id="contact-form"> <input type="email" name="email" required> <input type="text" name="name" required> <textarea name="message" required></textarea> <button type="submit">Send</button> </form> <script> document.getElementById('contact-form').addEventListener('submit', async (e) => { e.preventDefault() const formData = new FormData(e.target) const data = Object.fromEntries(formData) // Add auto-responder message data._autorespond = 'Thanks for reaching out! I\'ll get back to you soon.' const response = await fetch('YOUR_VAL_URL/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) if (response.ok) { alert('Message sent!') e.target.reset() } }) </script>

With Axios

Create val
axios.post('YOUR_VAL_URL/submit', { email: 'user@example.com', name: 'John Doe', message: 'Hello!', _autorespond: 'Thanks! Will get back to you in a jiffy.' })

Svelte Component (Import from Val Town)

<script> import FormSubmission from 'https://yourval.web.val.run/frontend/FormSubmission.svelte' </script> <!-- Basic (contact preset: name, email, message) --> <FormSubmission service_url="https://yourval.web.val.run" /> <!-- Custom fields --> <FormSubmission service_url="https://yourval.web.val.run" fields={[ { name: 'name', label: 'Name', type: 'text', required: true }, { name: 'email', label: 'Email', type: 'email', required: true }, { name: 'phone', label: 'Phone', type: 'tel', required: false }, { name: 'message', label: 'Message', type: 'textarea', required: true, rows: 5 } ]} /> <!-- Fully themed --> <div style=" --button-bg: #10b981; --button-hover-bg: #059669; --input-border-radius: 8px; --form-max-width: 600px; "> <FormSubmission service_url="https://yourval.web.val.run" /> </div>

Available CSS Variables:

  • --form-max-width - Form container width (default: 500px)
  • --form-font-family - Font family
  • --form-group-spacing - Space between fields (default: 1.5rem)
  • --label-color - Label text color
  • --label-font-size - Label size
  • --label-font-weight - Label weight
  • --input-padding - Input padding
  • --input-border - Input border
  • --input-border-radius - Input corner radius
  • --input-bg - Input background
  • --input-color - Input text color
  • --input-focus-border-color - Border color on focus
  • --input-focus-shadow - Shadow on focus
  • --button-bg - Button background
  • --button-hover-bg - Button hover background
  • --button-color - Button text color
  • --button-padding - Button padding
  • --button-border-radius - Button corner radius
  • --success-bg / --success-color - Success message colors
  • --error-bg / --error-color - Error message colors

Endpoints

  • POST /submit - Submit a form
  • GET /admin - View all submissions (admin dashboard)
  • GET /portal/:token - Client portal (unique per domain)
  • GET /api/submissions - Get submissions as JSON
  • GET /api/portal/:token/submissions - Get submissions for specific portal

Special Fields

  • email - User's email (required for auto-responder)
  • _autorespond - Custom message to send back to the user
  • _honeypot or honeypot - Spam trap field (leave empty, hide with CSS)

Spam Protection

Add a honeypot field to your forms:

<input type="text" name="_honeypot" style="display:none" tabindex="-1" autocomplete="off">

Rate limiting: 5 submissions per minute per IP

Organization

Submissions are automatically organized by:

  • Domain - Extracted from the referer/origin header
  • Page - The specific page path where the form was submitted
  • Timestamp - When the submission was received

Future Enhancements

  • Export to Google Sheets
  • Delete submissions
  • Domain whitelist
  • Custom email templates
  • Webhook integrations
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
PricingNewsletterBlogAboutCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
Β© 2025 Val Town, Inc.