Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Concierge Worker

A Cloudflare Worker in Rust that provides forms, calendars, and event/venue booking. Embed forms and calendars in your sites using HTMX or iframes.

Features

Forms

  • Dynamic Forms - Create forms with custom fields via admin dashboard
  • Field Types - Text, email, mobile, long text, file upload
  • Customizable Styling - Full CSS variable control per form
  • File Uploads - R2 storage with configurable size limits
  • Auto-Responders - Send acknowledgement messages via SMS, WhatsApp, or Email
  • Digest Notifications - Receive daily or weekly summaries of submissions

Calendars & Bookings

  • Multiple Calendars - Create and manage independent calendars
  • Booking Links - Public forms for users to book time slots
  • Booking Approval - Auto-accept or require manual admin approval
  • View Links - Embeddable calendar views (week, month, year, list)
  • iCal Feeds - Subscribe to calendars from other apps
  • Instagram Integration - Auto-import events from posts using AI

Admin

  • Dashboard - Manage all forms and calendars in one place
  • Dark Mode - Automatic dark mode based on system preference
  • Cloudflare Access - Secure authentication via Zero Trust

Quick Start

  1. Deploy to Cloudflare
  2. Configure secrets
  3. Create your first form
  4. Embed in your site

Architecture

┌─────────────────────────────────────────────────────────┐
│                    Cloudflare Edge                       │
├─────────────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────┐  │
│  │   Worker    │  │     D1      │  │       KV        │  │
│  │   (Rust)    │──│  (SQLite)   │  │   (Key-Value)   │  │
│  └─────────────┘  └─────────────┘  └─────────────────┘  │
│         │                                    │          │
│  ┌─────────────┐                    ┌─────────────────┐ │
│  │     R2      │                    │  Cloudflare AI  │ │
│  │  (Storage)  │                    │   (Instagram)   │ │
│  └─────────────┘                    └─────────────────┘ │
└─────────────────────────────────────────────────────────┘
  • D1 - Stores bookings and form submissions
  • KV - Stores form/calendar configurations
  • R2 - Stores file uploads
  • AI - Extracts events from Instagram posts

Deployment

Prerequisites

  • Cloudflare account
  • Nix with flakes enabled (recommended)
  • Or: Rust toolchain with wasm32-unknown-unknown target

Setup

1. Clone and enter dev environment

git clone https://github.com/ananthb/concierge-worker.git
cd concierge-worker

# Using Nix (recommended)
nix develop

# Or using direnv
direnv allow

2. Create Cloudflare resources

# Create D1 database
wrangler d1 create concierge-worker

# Create KV namespace
wrangler kv namespace create concierge-worker

# Create R2 bucket (for file uploads)
wrangler r2 bucket create concierge-worker-uploads

3. Update wrangler.toml

Replace the placeholder IDs with the values from the commands above:

[[d1_databases]]
binding = "DB"
database_name = "concierge-worker"
database_id = "YOUR_DATABASE_ID"  # from wrangler d1 create

[[kv_namespaces]]
binding = "KV"
id = "YOUR_KV_ID"  # from wrangler kv namespace create

[[r2_buckets]]
binding = "UPLOADS"
bucket_name = "concierge-worker-uploads"

4. Run database migrations

wrangler d1 migrations apply concierge-worker

5. Deploy

Option A: Deploy from local machine

wrangler deploy

Option B: Deploy via GitHub Actions

  1. Go to your GitHub repo Settings > Secrets and variables > Actions
  2. Add a secret named CLOUDFLARE_API_TOKEN with a Cloudflare API token
  3. Push to main branch to trigger automatic deployment

Cloudflare Access Setup

The admin dashboard requires Cloudflare Access for authentication.

  1. Go to Cloudflare Zero Trust
  2. Navigate to Access > Applications
  3. Click Add an application > Self-hosted
  4. Configure:
    • Application name: Concierge Worker Admin
    • Session duration: 24 hours (or your preference)
    • Application domain: your-worker.your-subdomain.workers.dev
    • Path: /admin
  5. Add a policy to control who can access (e.g., email ends with @yourdomain.com)
  6. Save

Local Development

# Run locally with simulated bindings
wrangler dev

# Run locally with real D1/KV/R2 (requires account)
wrangler dev --remote

For local development, set ENVIRONMENT = "development" in wrangler.toml to bypass Access authentication.

Environment Variables

VariableDescriptionRequired
ENVIRONMENTSet to development to bypass authNo

See Configuration for secrets setup.

Configuration

All sensitive configuration is stored as Cloudflare Workers secrets.

Setting Secrets

Use the Wrangler CLI to set secrets:

wrangler secret put SECRET_NAME

You’ll be prompted to enter the value securely.

Environment Variables

VariableDescriptionRequired
ENVIRONMENTSet to development to bypass authNo

All Secrets Reference

SecretPurpose
TWILIO_SIDTwilio Account SID
TWILIO_TOKENTwilio Auth Token
TWILIO_FROM_SMSSMS sender number (e.g., +15551234567)
TWILIO_FROM_WHATSAPPWhatsApp sender (e.g., whatsapp:+15551234567)
TWILIO_FROM_EMAILEmail sender address for Twilio/SendGrid
SENDGRID_API_KEYSendGrid API key (for Twilio email)
WHATSAPP_ACCESS_TOKENWhatsApp Business API access token
WHATSAPP_PHONE_NUMBER_IDWhatsApp Business API phone number ID
RESEND_API_KEYResend API key
RESEND_FROMResend sender address
GOOGLE_SERVICE_ACCOUNT_JSONGoogle service account JSON (for Sheets)
ENCRYPTION_KEYToken encryption key (for Instagram)
INSTAGRAM_APP_IDMeta/Instagram app ID
INSTAGRAM_APP_SECRETMeta/Instagram app secret

Notification Channels

Important: The Responders and Digest tabs in the admin UI are only visible when at least one notification channel is configured.

Twilio SMS

wrangler secret put TWILIO_SID
wrangler secret put TWILIO_TOKEN
wrangler secret put TWILIO_FROM_SMS

Twilio WhatsApp

wrangler secret put TWILIO_SID
wrangler secret put TWILIO_TOKEN
wrangler secret put TWILIO_FROM_WHATSAPP

WhatsApp Business API (Meta)

For direct WhatsApp Business API integration:

wrangler secret put WHATSAPP_ACCESS_TOKEN
wrangler secret put WHATSAPP_PHONE_NUMBER_ID

Twilio Email (via SendGrid)

wrangler secret put SENDGRID_API_KEY
wrangler secret put TWILIO_FROM_EMAIL

Resend Email

wrangler secret put RESEND_API_KEY
wrangler secret put RESEND_FROM

Channel Detection

The admin UI automatically detects which channels are available:

ChannelRequired Secrets
Twilio SMSTWILIO_SID + TWILIO_FROM_SMS
Twilio WhatsAppTWILIO_SID + TWILIO_FROM_WHATSAPP
Twilio EmailSENDGRID_API_KEY + TWILIO_FROM_EMAIL
Resend EmailRESEND_API_KEY + RESEND_FROM

Google Sheets Integration

For syncing form submissions to Google Sheets:

wrangler secret put GOOGLE_SERVICE_ACCOUNT_JSON

The value should be the entire JSON content of your Google Cloud service account key file.

Instagram Integration

For automatic event extraction from Instagram posts:

# Generate with: openssl rand -hex 32
wrangler secret put ENCRYPTION_KEY

wrangler secret put INSTAGRAM_APP_ID
wrangler secret put INSTAGRAM_APP_SECRET

Setting up Meta App

  1. Go to Meta for Developers
  2. Create an app with Instagram Basic Display product
  3. Configure OAuth redirect URI: https://your-worker.workers.dev/instagram/callback
  4. Add test users while in development mode

Forms

Create dynamic forms with custom fields, styling, and automated responses.

Creating a Form

  1. Go to the admin dashboard (/admin)
  2. Click + Create Form
  3. Configure the form settings

Form Settings

Basic Settings

FieldDescription
Form NameInternal name (shown in admin)
SlugURL path (e.g., contact/f/contact)
Form TitleDisplayed to users
Submit Button TextButton label
Success MessageShown after submission
Allowed OriginsDomains that can embed this form
Google Sheet URLOptional: sync submissions to a spreadsheet

Field Types

TypeHTML InputValidation
Text<input type="text">None
Email<input type="email">Email format
Mobile<input type="tel">Phone format
Long Text<textarea>None
File<input type="file">Size limit

Field Configuration

Each field has:

  • Label - Displayed above the input
  • Field ID - Used in templates and data (e.g., name, email)
  • Type - Input type (see above)
  • Required - Whether the field must be filled

Styling

Forms can be styled using CSS variables. See CSS Customization for details.

Show/Hide Title

Toggle “Show form title” in the Styling tab to hide the title when embedding.

Custom CSS

Add custom CSS rules in the Styling tab:

.contact-form {
    max-width: 500px;
}
button {
    text-transform: uppercase;
}

Viewing Responses

  1. Go to admin dashboard
  2. Click Responses next to the form
  3. View all submissions in a table

Archiving Forms

Archive forms to make them read-only without deleting data:

  1. Click Archive in the dashboard
  2. Form becomes read-only
  3. Click Unarchive to restore

Deleting Forms

Warning: This permanently deletes the form and all submissions.

  1. Open the form editor
  2. Scroll to Danger Zone
  3. Click Delete Form

Calendars & Bookings

Create calendars with time slots for appointments, events, or venue bookings.

Creating a Calendar

  1. Go to the admin dashboard (/admin)
  2. Click + New Calendar
  3. Configure calendar settings

Calendar Settings

Basic Settings

FieldDescription
NameCalendar name
DescriptionOptional description
TimezoneCalendar timezone
Allowed DomainsDomains that can embed
Custom CSSAdditional styling

Time Slots

Configure when bookings are available:

  1. Go to calendar editor
  2. Click Bookings tab
  3. Click Configure Available Slots

Recurring Slots

Set weekly availability:

FieldDescription
Day of WeekMonday, Tuesday, etc.
Start TimeWhen slots begin
End TimeWhen slots end
DurationLength of each slot (minutes)
CapacityMax bookings per slot

Specific Date Slots

Add availability for specific dates (overrides weekly pattern).

Public pages where users can book time slots.

  1. Go to calendar editor
  2. Click Bookings tab
  3. Click + Add Booking Link
FieldDescription
NameLink name
SlugURL path
DurationBooking duration (minutes)
Confirmation MessageShown after booking
Hide TitleHide name when embedded
Auto-AcceptConfirm immediately or require approval

Custom Fields

Add fields to collect information during booking:

  • Name, Email, Phone (built-in)
  • Custom text fields
  • Notes/comments

Booking Approval

When Auto-Accept is disabled:

  1. Booking is created with “Pending” status
  2. Admin receives notification (if configured)
  3. Admin clicks approval link
  4. Customer receives confirmation

Public calendar views that can be embedded or shared.

View Types

TypeDescription
Week7-day view with time grid
MonthMonthly calendar grid
YearYearly overview
ListScrolling list of events
  1. Go to calendar editor
  2. Click Settings tab
  3. Click + Add View Link

iCal Feeds

Subscribe to the calendar from other apps (Google Calendar, Apple Calendar, etc.):

  1. Go to calendar editor
  2. Click Settings tab
  3. Click + Add Feed Link
  4. Copy the feed URL with token

Events

Manual Events

Create events directly:

  1. Go to calendar editor
  2. Click Events tab
  3. Click Open Event Editor

Instagram Integration

Automatically import events from Instagram posts:

  1. Configure Instagram secrets (see Configuration)
  2. Go to Events tab
  3. Click Connect Instagram Account
  4. Authorize access
  5. Events are synced hourly via cron trigger

Auto-Responders

Send automatic messages when forms are submitted or bookings are made.

Prerequisites

Configure at least one notification channel. See Configuration.

Note: The Responders tab only appears when channels are configured.

Form Responders

Send acknowledgement messages to form submitters.

Setup

  1. Open form editor
  2. Go to Responders tab
  3. Click + Add Responder

Configuration

FieldDescription
NameInternal name
ChannelSMS, WhatsApp, or Email
Target FieldForm field containing recipient (email/phone)
SubjectEmail subject (email only)
BodyMessage content
EnabledToggle on/off
Use AIGenerate response with AI

Template Placeholders

Use {{field_id}} to insert field values:

Hi {{name}},

Thank you for contacting us. We received your message:

"{{message}}"

We'll get back to you at {{email}} soon.

Booking Responders

Customer Notifications

Send confirmation to customers when bookings are confirmed:

  1. Open booking link editor
  2. Find Customer Notifications section
  3. Click + Add Responder

Available placeholders:

PlaceholderDescription
{{name}}Customer name
{{email}}Customer email
{{date}}Booking date
{{time}}Booking time

Example:

Hi {{name}},

Your booking for {{date}} at {{time}} has been confirmed.

Thank you!

Admin Notifications

Receive notifications when bookings need approval:

  1. Open booking link editor
  2. Disable Auto-Accept
  3. Find Admin Notifications section
  4. Click + Add Responder

Additional placeholder:

PlaceholderDescription
{{approve_url}}Link to approve booking

Example:

New booking request:

Name: {{name}}
Email: {{email}}
Date: {{date}}
Time: {{time}}

Approve: {{approve_url}}

AI-Powered Responses

Enable Use AI to generate personalized responses:

  1. Toggle Use AI on
  2. Write a system prompt describing desired behavior
  3. The AI uses form data to generate contextual responses

Example prompt:

You are a helpful assistant for a photography studio.
Thank the customer for their inquiry and provide a brief,
friendly response based on their message.
Keep it under 3 sentences.

Channel-Specific Notes

SMS

  • Keep messages under 160 characters for single-segment
  • Longer messages are split and may cost more

WhatsApp

  • Must use approved message templates for first contact
  • Can send free-form messages within 24-hour window

Email

  • Include unsubscribe info for marketing emails
  • HTML formatting is supported

Digests

Receive periodic summaries of new form submissions and bookings.

Prerequisites

Configure at least one notification channel. See Configuration.

Note: The Digest tab only appears when channels are configured.

Form Digests

Get summaries of new form submissions.

Setup

  1. Open form editor
  2. Go to Digest tab
  3. Select frequency
  4. Add recipients

Configuration

FieldDescription
FrequencyNone, Daily, or Weekly
RecipientsList of notification recipients

Adding Recipients

  1. Click + Add Recipient
  2. Configure:
    • Name - Recipient identifier
    • Channel - SMS, WhatsApp, or Email
    • Address - Phone number or email
    • Enabled - Toggle on/off

Digest Content

Form digests include:

  • Form name
  • Number of new submissions
  • For each submission:
    • Timestamp
    • All field values

Example email digest:

New submissions for form: Contact Form

You have 3 new response(s) since the last digest.

--- Response #1 (2024-03-15 10:30:00) ---
Name: John Doe
Email: john@example.com
Message: I'd like to learn more about your services.

--- Response #2 (2024-03-15 14:45:00) ---
Name: Jane Smith
Email: jane@example.com
Message: Do you offer weekend appointments?

...

Booking Digests

Get summaries of new bookings.

Setup

  1. Open calendar editor
  2. Go to Digest tab
  3. Select frequency
  4. Add recipients

Digest Content

Booking digests include:

  • Calendar name
  • Number of new bookings
  • For each booking:
    • Timestamp
    • Customer name and contact
    • Date and time
    • Duration
    • Status (Pending/Confirmed)
    • Notes (if any)

Example:

New bookings for calendar: Appointments

You have 2 new booking(s) since the last digest.

--- Booking #1 (2024-03-15 09:00:00) ---
Name: John Doe
Email: john@example.com
Phone: +1234567890
Date: 2024-03-20
Time: 2:00 PM
Duration: 30 minutes
Status: Confirmed

--- Booking #2 (2024-03-15 11:30:00) ---
Name: Jane Smith
Email: jane@example.com
Date: 2024-03-21
Time: 10:00 AM
Duration: 60 minutes
Status: Pending
Notes: First-time consultation

Frequency

OptionWhen Sent
NoneDisabled
DailyEvery day (via cron)
WeeklyEvery week (via cron)

Digests are sent via Cloudflare’s cron trigger. The exact timing depends on your cron configuration in wrangler.toml:

[triggers]
crons = ["0 * * * *"]  # Every hour

Multiple Recipients

Add multiple recipients to send the same digest to different people or channels:

  • Send email to admin and SMS to on-call staff
  • CC multiple team members
  • Use different channels for redundancy

iframe Embedding

Embed forms, booking pages, and calendar views using iframes.

Basic Usage

Forms

<iframe
    src="https://your-worker.workers.dev/f/contact"
    style="border: none; width: 100%; min-height: 500px;">
</iframe>

Booking Forms

<iframe
    src="https://your-worker.workers.dev/book/{calendar_id}/{slug}"
    style="border: none; width: 100%; min-height: 600px;">
</iframe>

Calendar Views

<iframe
    src="https://your-worker.workers.dev/view/{calendar_id}/{slug}"
    style="border: none; width: 100%; min-height: 500px;">
</iframe>

Auto-Resizing

iframes don’t automatically resize to fit content. Options:

1. Fixed Height

Set a minimum height that accommodates your content:

<iframe src="..." style="min-height: 600px;"></iframe>

2. JavaScript Resize

Use a library like iframe-resizer:

<script src="https://cdn.jsdelivr.net/npm/iframe-resizer@4/js/iframeResizer.min.js"></script>
<iframe id="form" src="..."></iframe>
<script>iFrameResize({}, '#form')</script>

Query Parameters

Customize appearance via URL parameters:

Inline CSS

<iframe src="https://your-worker.workers.dev/f/contact?css=button{background:green}"></iframe>

External Stylesheet

<iframe src="https://your-worker.workers.dev/f/contact?css_url=https://example.com/form.css"></iframe>

Hide Title

<iframe src="https://your-worker.workers.dev/book/{id}/{slug}?hide_title=true"></iframe>

Calendar View Options

<!-- Start on specific date -->
<iframe src=".../view/{id}/{slug}?date=2024-03-15"></iframe>

<!-- Force specific view -->
<iframe src=".../view/{id}/{slug}?view=month"></iframe>

<!-- Booking: show more days -->
<iframe src=".../book/{id}/{slug}?days=14"></iframe>

See Query Parameters for full reference.

Cross-Origin Considerations

Allowed Origins

For security, configure allowed origins in the form/calendar settings:

  1. Open form or calendar editor
  2. Add your domain to Allowed Origins
  3. Leave empty to allow all (not recommended for production)

Example:

https://example.com
https://www.example.com

Third-party cookies may be blocked by browsers. This can affect:

  • Session persistence
  • CSRF protection

Solutions:

  1. Use HTMX embedding instead (same-origin)
  2. Host the worker on a subdomain of your site
  3. Use the form as a standalone page with redirect

Styling Tips

Remove iframe Border

<iframe src="..." style="border: none;"></iframe>

Responsive Width

<iframe src="..." style="width: 100%; max-width: 600px;"></iframe>

Transparent Background

The form/calendar will use its configured background color. To make it transparent:

<iframe src="...?css=body{background:transparent}"></iframe>

Then ensure your page has a background color.

Example: Complete Form Embed

<!DOCTYPE html>
<html>
<head>
    <style>
        .form-container {
            max-width: 600px;
            margin: 2rem auto;
        }
        .form-container iframe {
            border: none;
            width: 100%;
            min-height: 500px;
        }
    </style>
</head>
<body>
    <div class="form-container">
        <iframe src="https://your-worker.workers.dev/f/contact?css=body{background:transparent}"></iframe>
    </div>
</body>
</html>

HTMX Embedding

Embed forms and calendars directly into your page using HTMX for a seamless experience.

Why HTMX?

Compared to iframes:

  • No iframe sizing issues - Content is part of your page
  • Better styling control - Your CSS applies directly
  • Smoother interactions - No iframe refresh on submit
  • SEO friendly - Content is in the DOM

Setup

1. Include HTMX

Add HTMX to your page:

<script src="https://unpkg.com/htmx.org@1.9.10"></script>

2. Configure Allowed Origins

Add your domain to the form/calendar’s Allowed Origins setting:

https://example.com
https://www.example.com

Forms

Basic Embed

<div hx-get="https://your-worker.workers.dev/f/contact"
     hx-trigger="load"
     hx-swap="innerHTML">
    Loading form...
</div>

With Loading Indicator

<div hx-get="https://your-worker.workers.dev/f/contact"
     hx-trigger="load"
     hx-swap="innerHTML"
     hx-indicator="#form-loading">
    <div id="form-loading" class="htmx-indicator">
        Loading...
    </div>
</div>

Calendar Views

<div hx-get="https://your-worker.workers.dev/view/{calendar_id}/{slug}"
     hx-trigger="load"
     hx-swap="innerHTML">
    Loading calendar...
</div>

With Date Parameter

<div hx-get="https://your-worker.workers.dev/view/{calendar_id}/{slug}?date=2024-03-15"
     hx-trigger="load"
     hx-swap="innerHTML">
</div>

Booking Forms

<div hx-get="https://your-worker.workers.dev/book/{calendar_id}/{slug}"
     hx-trigger="load"
     hx-swap="innerHTML">
    Loading booking form...
</div>

Styling

Since HTMX content is part of your page, you have full CSS control.

Override CSS Variables

<style>
    /* Form variables */
    :root {
        --cf-primary-color: #007bff;
        --cf-border-radius: 8px;
    }

    /* Calendar variables */
    :root {
        --cal-primary: #28a745;
        --cal-bg: transparent;
    }
</style>

Target Form Elements

<style>
    .contact-form {
        max-width: 500px;
        margin: 0 auto;
    }

    .contact-form button {
        text-transform: uppercase;
    }

    .form-group label {
        font-weight: bold;
    }
</style>

Hide Title via CSS

<style>
    .contact-form h1 {
        display: none;
    }
</style>

Query Parameters

Pass parameters to customize the embedded content:

<!-- Hide title -->
<div hx-get=".../f/contact?hide_title=true" ...></div>

<!-- Custom view -->
<div hx-get=".../view/{id}/{slug}?view=month" ...></div>

<!-- Inline CSS override -->
<div hx-get=".../f/contact?css=button{background:green}" ...></div>

Form Submission

Forms use HTMX’s hx-post for submission:

  1. User fills form
  2. Submit button triggers HTMX POST
  3. Success message replaces form
  4. After 3 seconds, form reloads automatically

Custom Success Handling

Override the default behavior:

<div hx-get="https://your-worker.workers.dev/f/contact"
     hx-trigger="load"
     hx-swap="innerHTML"
     hx-on::after-settle="handleFormLoad(event)">
</div>

<script>
function handleFormLoad(event) {
    // Add custom event listeners to the loaded form
    const form = event.target.querySelector('form');
    if (form) {
        form.addEventListener('htmx:afterRequest', function(e) {
            if (e.detail.successful) {
                // Custom success handling
                console.log('Form submitted successfully');
            }
        });
    }
}
</script>

Error Handling

Handle network errors gracefully:

<div hx-get="https://your-worker.workers.dev/f/contact"
     hx-trigger="load"
     hx-swap="innerHTML"
     hx-on::response-error="this.innerHTML = 'Failed to load form'">
    Loading...
</div>

Complete Example

<!DOCTYPE html>
<html>
<head>
    <script src="https://unpkg.com/htmx.org@1.9.10"></script>
    <style>
        .form-container {
            max-width: 600px;
            margin: 2rem auto;
            padding: 1rem;
        }

        /* Override form styles */
        :root {
            --cf-primary-color: #6366f1;
            --cf-border-radius: 12px;
            --cf-bg-color: transparent;
        }

        .contact-form {
            background: white;
            padding: 2rem;
            border-radius: 12px;
            box-shadow: 0 4px 6px rgba(0,0,0,0.1);
        }

        .htmx-indicator {
            display: none;
        }
        .htmx-request .htmx-indicator {
            display: block;
        }
    </style>
</head>
<body>
    <div class="form-container">
        <div hx-get="https://your-worker.workers.dev/f/contact"
             hx-trigger="load"
             hx-swap="innerHTML"
             hx-indicator=".loading">
            <div class="loading htmx-indicator">Loading form...</div>
        </div>
    </div>
</body>
</html>

CSS Customization

Customize the appearance of forms, calendars, and booking pages.

Customization Methods

1. Admin Settings

Edit CSS variables and custom CSS in the admin UI:

  1. Open form or calendar editor
  2. Go to Styling tab
  3. Modify CSS variables or add custom CSS

2. Query Parameters

Override styles via URL:

<!-- Inline CSS -->
<iframe src=".../f/contact?css=button{background:green}"></iframe>

<!-- External stylesheet -->
<iframe src=".../f/contact?css_url=https://example.com/style.css"></iframe>

3. Page CSS (HTMX only)

When using HTMX embedding, your page CSS applies directly.

CSS Variables

Form Variables

VariableDescriptionDefault
--cf-font-familyFont familyinherit
--cf-font-sizeBase font size1rem
--cf-text-colorText color#333333
--cf-bg-colorPage backgroundtransparent
--cf-form-bgForm background#ffffff
--cf-border-colorBorder color#dddddd
--cf-border-radiusBorder radius4px
--cf-primary-colorButton color#0070f3
--cf-primary-hoverButton hover#0060df
--cf-input-paddingInput padding0.5rem

Calendar Variables

VariableDescriptionDefault
--cal-primaryAccent color#0070f3
--cal-textText color#333333
--cal-bgBackground#ffffff
--cal-border-radiusBorder radius4px
--cal-fontFont familysystem-ui

See CSS Variables Reference for the complete auto-generated list.

CSS Classes

Form Classes

ClassElement
.contact-formForm container
.form-groupField wrapper
labelField labels
input, textareaInput fields
buttonSubmit button
.successSuccess message
.errorError message

Calendar Classes

ClassElement
.calendar-viewMain container
.calendar-headerNavigation header
.calendar-gridDay grid
.eventEvent item
.booking-formBooking form
.time-slotsTime slot container
.time-slotIndividual slot button

Examples

Dark Theme Form

:root {
    --cf-bg-color: #1a1a1a;
    --cf-form-bg: #2d2d2d;
    --cf-text-color: #e0e0e0;
    --cf-border-color: #444444;
    --cf-primary-color: #3b9eff;
}

Rounded Modern Style

:root {
    --cf-border-radius: 12px;
    --cf-input-padding: 0.75rem;
}

.contact-form {
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

button {
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
}

Minimal Calendar

:root {
    --cal-primary: #000000;
    --cal-bg: transparent;
    --cal-border-radius: 0;
}

.event {
    border-left: 3px solid var(--cal-primary);
    background: transparent;
}

Brand Colors

/* Use your brand colors */
:root {
    --cf-primary-color: #6366f1;  /* Indigo */
    --cf-primary-hover: #4f46e5;
    --cal-primary: #6366f1;
}

Using External Stylesheets

Host a CSS file and reference it:

<iframe src=".../f/contact?css_url=https://example.com/brand.css"></iframe>

Your external CSS can override any styles:

/* brand.css */
:root {
    --cf-primary-color: #your-color;
}

.contact-form {
    font-family: 'Your Font', sans-serif;
}

Responsive Design

Forms and calendars are responsive by default. Override breakpoints:

@media (max-width: 600px) {
    .contact-form {
        padding: 1rem;
    }

    .time-slots {
        grid-template-columns: repeat(2, 1fr);
    }
}

Hiding Elements

Hide Title

Via query param:

?hide_title=true

Via CSS:

.contact-form h1,
.booking-form h1 {
    display: none;
}

Hide Specific Fields

.form-group:has(input[name="phone"]) {
    display: none;
}

Add print-specific styles:

@media print {
    .calendar-header button {
        display: none;
    }

    .event {
        break-inside: avoid;
    }
}

CSS Variables Reference

This page is auto-generated from the source code.

Form CSS Variables

Use these variables to customize form appearance:

VariableDescription
--cf-bg-colorPage background color
--cf-border-colorInput border color
--cf-border-radiusBorder radius
--cf-font-familyFont family
--cf-font-sizeBase font size
--cf-form-bgForm container background
--cf-input-paddingInput padding
--cf-primary-colorPrimary/button color
--cf-primary-hoverPrimary hover color
--cf-text-colorMain text color

Calendar CSS Variables

Use these variables to customize calendar and booking views:

VariableDescription
--cal-bgBackground color
--cal-border-radiusBorder radius
--cal-fontFont family
--cal-primaryPrimary/accent color
--cal-textText color

Usage Examples

Override via Query Parameter

<iframe src=".../f/contact?css=--cf-primary-color:green"></iframe>

Override in Page CSS (HTMX)

:root {
    --cf-primary-color: #6366f1;
    --cal-primary: #6366f1;
}

Override in Admin Settings

  1. Open form/calendar editor
  2. Go to Styling tab
  3. Add Custom CSS:
:root {
    --cf-primary-color: #your-color;
}

Query Parameters Reference

This page is auto-generated from the source code.

Form Embedding

ParameterDescriptionExample
cssInline CSS to apply?css=button{background:green}
css_urlURL to external stylesheet?css_url=https://example.com/style.css

Calendar & Booking Views

ParameterDescriptionExample
cssInline CSS to apply?css=.event{color:blue}
css_urlURL to external stylesheet?css_url=https://example.com/cal.css
dateStarting date for view?date=2024-03-15
viewView type (week/month/year/endless)?view=month
daysNumber of days to show (booking)?days=14
hide_titleHide the title?hide_title=true

iCal Feeds

ParameterDescriptionExample
tokenAuthentication token?token=abc123

Combining Parameters

Multiple parameters can be combined:

/f/contact?css=button{background:green}&hide_title=true
/view/{id}/{slug}?date=2024-03-15&view=month&css_url=https://example.com/cal.css

Channel Configuration Reference

This page is auto-generated from the source code.

Available Channels

The following notification channels are supported:

ChannelProviderUse Case
twilio_smsTwilioSMS to mobile numbers
twilio_whatsappTwilioWhatsApp via Twilio
meta_whatsappMetaWhatsApp Business API
twilio_emailSendGrid via TwilioEmail notifications
resend_emailResendEmail notifications

Required Secrets

Twilio SMS

wrangler secret put TWILIO_SID
wrangler secret put TWILIO_TOKEN
wrangler secret put TWILIO_FROM_SMS        # e.g., +15551234567

Twilio WhatsApp

wrangler secret put TWILIO_SID
wrangler secret put TWILIO_TOKEN
wrangler secret put TWILIO_FROM_WHATSAPP   # e.g., whatsapp:+15551234567

WhatsApp Business API (Meta)

wrangler secret put WHATSAPP_ACCESS_TOKEN
wrangler secret put WHATSAPP_PHONE_NUMBER_ID

Twilio Email (via SendGrid)

wrangler secret put SENDGRID_API_KEY
wrangler secret put TWILIO_FROM_EMAIL      # e.g., noreply@yourdomain.com

Resend Email

wrangler secret put RESEND_API_KEY
wrangler secret put RESEND_FROM            # e.g., noreply@yourdomain.com

Channel Detection

The admin UI shows Responders and Digest tabs only when channels are available:

ChannelDetection
Twilio SMSTWILIO_SID and TWILIO_FROM_SMS are set
Twilio WhatsAppTWILIO_SID and TWILIO_FROM_WHATSAPP are set
Twilio EmailSENDGRID_API_KEY and TWILIO_FROM_EMAIL are set
Resend EmailRESEND_API_KEY and RESEND_FROM are set

Note: If you don’t see the Responders or Digest tabs in the admin UI, ensure at least one channel’s secrets are configured.

URL Routes Reference

This page is auto-generated from the source code.

Public Routes

RouteMethodDescription
/f/{slug}GETRender public form
/f/{slug}/submitPOSTSubmit form
/book/{calendar_id}/{slug}GETRender booking form
/book/{calendar_id}/{slug}/submitPOSTSubmit booking
/book/{calendar_id}/{slug}/approve/{booking_id}GETApprove pending booking
/book/{calendar_id}/{slug}/cancel/{booking_id}GETCancel booking
/view/{calendar_id}/{slug}GETRender calendar view
/feed/{calendar_id}/{slug}GETiCal feed

Admin Routes

RouteMethodDescription
/adminGETAdmin dashboard
/admin/forms/newGETNew form editor
/admin/forms/{slug}GETEdit form
/admin/forms/{slug}PUTUpdate form
/admin/forms/{slug}DELETEDelete form
/admin/forms/{slug}/responsesGETView form responses
/admin/forms/{slug}/archivePOSTArchive form
/admin/forms/{slug}/unarchivePOSTUnarchive form
/admin/calendarsPOSTCreate calendar
/admin/calendars/{id}GETEdit calendar
/admin/calendars/{id}PUTUpdate calendar
/admin/calendars/{id}DELETEDelete calendar
/admin/calendars/{id}/archivePOSTArchive calendar
/admin/calendars/{id}/unarchivePOSTUnarchive calendar
/admin/calendars/{id}/bookingsGETView all bookings
/admin/calendars/{id}/slotsGETConfigure time slots
/admin/calendars/{id}/eventsGETManage events
/admin/calendars/{id}/bookingPOSTCreate booking link
/admin/calendars/{id}/booking/{link_id}GETEdit booking link
/admin/calendars/{id}/viewPOSTCreate view link
/admin/calendars/{id}/view/{link_id}GETEdit view link
/admin/calendars/{id}/feedPOSTCreate feed link

Instagram Routes

RouteMethodDescription
/instagram/auth/{calendar_id}GETStart Instagram OAuth
/instagram/callbackGETOAuth callback
/instagram/disconnect/{calendar_id}/{source_id}DELETEDisconnect account

Static Assets

RouteDescription
/logo.svgApplication logo