An account-free, self-hosted When2Meet alternative for modern teams.
Create the board. Share the link. Find the overlap.
tempoll is a modern When2Meet-style scheduler: organizers create an event, participants join with just a name, and everyone edits availability on a live heatmap.
Use tempoll for free at tempoll.app.
Quick start · How it works · Self-hosting · Configuration
tempoll lets an organizer create a scheduling board, share one public link, and collect availability on a live heatmap. Participants join with just a name. The organizer gets a private manage URL to rename or remove participants and open or close the event.
GitHub-ready for self-hosting with the included
docker-compose.yamland a first-run setup flow built for Coolify.
The project is built for two audiences:
- operators who want to self-host a polished scheduling app with a simple Postgres stack
- maintainers who want a clean foundation for a hosted version without losing the product's current UX contract
- No required login. Public participants join with a name only.
- Private organizer management URL, separate from the public event URL.
- Realtime collaborative heatmap powered by Postgres
LISTEN/NOTIFYand Server-Sent Events. - Optional Resend-powered organizer digest emails with five-minute quiet-period batching.
- Ranked best meeting windows based on shared overlap.
- Date-range based event creation. One range expands into concrete event dates internally.
- Browser-local recent events history, including private organizer links clearly marked as sensitive.
- First-run setup wizard that generates only non-secret app and legal env values.
- Optional legal pages rendered from environment variables.
- Docker Compose and Coolify ready.
- Create an event with a title, timezone, one date range, a daily time window, slot size, and meeting duration.
- tempoll expands the date range into actual dates and creates:
- a public event URL
- a private organizer manage URL
- Participants open the public page, enter their name, and paint availability directly on the heatmap.
- Everyone sees updates in real time.
- The organizer uses the private manage page to share links, rename or remove participants, and close the board when scheduling is done.
Next.js 16React 19Tailwind CSS 4shadcn/uiandRadix UIPrisma 7PostgreSQL 16pglistener for realtime fan-outVitestandTesting Library
Recommended local toolchain:
- Node.js 24+
pnpm- PostgreSQL 16
The local default database URL is:
postgresql://postgres:postgres@localhost:55432/tempoll?schema=publicThat non-default 55432 port is intentional to avoid common Postgres collisions.
- Copy the example env file:
cp .env.example .env.env.exampleand.env.coolify.exampleintentionally contain placeholders and local-only example values. Do not commit your real.env.- Participant edit links are consumed server-side and immediately redirected to the canonical event URL, so raw participant tokens do not remain in the rendered page URL.
- Organizer links are bearer secrets. They are stored only in browser-local recent history and are visibly marked as private in the UI.
- Run
pnpm security:scan-secretsandpnpm security:auditbefore publishing changes.
For parallel Git worktrees, use the worktree-aware dev command:
pnpm dev:worktreeIt starts a fresh Postgres container for the current worktree, picks free local ports,
applies migrations, seeds demo events, writes a managed block to .env.local, and
starts the Next dev server. The generated local env includes TEMPOLL_DEV_MODE=true,
which enables the /dev/database status page and footer link.
- Start a local Postgres instance however you prefer. Example with Docker:
docker run -d \
--name tempoll-postgres \
-e POSTGRES_DB=tempoll \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-p 55432:5432 \
postgres:16- Install dependencies and start the app:
pnpm install
pnpm devBy default, .env.example starts with APP_SETUP_COMPLETE=false, so normal routes redirect to /setup. That is expected.
- Either:
- use the setup wizard to generate your non-secret app config and copy the values into
.env, or - edit
.envmanually if you already know your final configuration
- Set
APP_SETUP_COMPLETE=true, then apply migrations and restart the app:
pnpm prisma:migrate
pnpm devIf you want a production-like local stack:
cp .env.coolify.example .env.coolify
export SERVICE_PASSWORD_TEMPOLL_DB=change-me
docker compose --env-file .env.coolify up -d --buildThen open /setup, copy the generated non-secret app config into your env file or deployment settings, switch APP_SETUP_COMPLETE=true, and redeploy or restart.
The repository ships with a root-level docker-compose.yaml that defines:
appdb
The compose setup derives DATABASE_URL internally from:
TEMPOLL_DB_NAMETEMPOLL_DB_USERSERVICE_PASSWORD_TEMPOLL_DB
Use .env.coolify.example as the operator-facing starting point. Do not ask operators to paste a full DATABASE_URL when they use the bundled database service.
tempoll targets Coolify via Docker Compose, not Nixpacks.
Recommended Coolify settings:
- Build Pack:
Docker Compose - Base Directory:
/ - Assign your public domain to the
appservice - Internal app port:
3000
Suggested first deploy flow:
- Add the repository as a Docker Compose application.
- Seed the environment from
.env.coolify.example. - Keep
APP_SETUP_COMPLETE=falsefor the first deploy. - Let Coolify generate
SERVICE_PASSWORD_TEMPOLL_DBfor a fresh stack. - Open
/setupon the deployed app. - Copy only the generated non-secret app config values into Coolify.
- Set
APP_SETUP_COMPLETE=true. - Redeploy.
Important operational note:
- Postgres credentials only initialize a fresh volume.
- If you already have a persistent database volume,
SERVICE_PASSWORD_TEMPOLL_DBmust match the current live password before redeploying. - If credentials drift, either align env vars with the existing database or reset the database volume deliberately.
Health checks stay available at /api/health, even while setup is incomplete.
| Variable | Required | Purpose |
|---|---|---|
APP_SETUP_COMPLETE |
Yes | Gates the whole app. Only the setup wizard and /api/health stay open when this is not exactly true. |
APP_NAME |
Yes | Product name shown in the UI. Defaults to tempoll. |
APP_URL |
Yes | Canonical public base URL used for generated links. |
LEGAL_PAGES_ENABLED |
No | Enables /imprint and /privacy. Defaults to false. |
DATAFAST_WEBSITE_ID |
No | Enables optional DataFast tracking when set together with DATAFAST_DOMAIN. |
DATAFAST_DOMAIN |
No | Domain sent to DataFast when optional tracking is enabled. |
DATABASE_URL |
Local dev or external DB only | Direct database connection string. |
TEMPOLL_DB_NAME |
Compose/Coolify | Bundled Postgres database name. |
TEMPOLL_DB_USER |
Compose/Coolify | Bundled Postgres database user. |
SERVICE_PASSWORD_TEMPOLL_DB |
Compose/Coolify | Bundled Postgres password secret. Use an underscore, not a hyphen. |
RESEND_API_KEY |
Optional | Enables organizer email digests when set together with RESEND_FROM_EMAIL. |
RESEND_FROM_EMAIL |
Optional | Verified sender address for organizer digest emails. |
RESEND_FROM_NAME |
Optional | Friendly sender name for organizer digest emails. Defaults to tempoll. |
DataFast tracking is disabled by default. It is enabled only when both variables are set:
DATAFAST_WEBSITE_ID=dfid_your_website_id
DATAFAST_DOMAIN=your-domain.comImplementation details in this repository:
- Script source:
https://datafa.st/js/script.cookieless.js - Loaded globally from the root layout via
next/script(afterInteractive) - Event endpoint proxied through
POST /api/datafast/events - Organizer routes (
/manage/[token]) are filtered and not forwarded to DataFast - CSP already allows
https://datafa.stinscript-srcandconnect-src
Organizer alert emails are disabled by default. They turn on only when both of these server-side variables are set:
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=notifications@example.com
RESEND_FROM_NAME=tempollNotes for operators:
/setupintentionally does not collect, render, or export any mail secrets.- The create flow and organizer manage page expose the email field only when the host is configured for delivery.
- Availability edits are batched into one digest after five quiet minutes instead of sending on every slot toggle.
- Digest emails include a private organizer CTA. Treat those inboxes as sensitive.
These fields are intentionally optional:
OPERATOR_LEGAL_NAMEOPERATOR_DISPLAY_NAMEOPERATOR_STREET_ADDRESSOPERATOR_POSTAL_CODEOPERATOR_CITYOPERATOR_COUNTRYOPERATOR_EMAILOPERATOR_PHONEOPERATOR_WEBSITEOPERATOR_BUSINESS_PURPOSEMEDIA_OWNEREDITORIAL_LINEPRIVACY_CONTACT_EMAILHOSTING_DESCRIPTIONPRIVACY_PROCESSORS
If legal pages are enabled and some of these fields are empty, tempoll falls back to softer wording such as "available on request" instead of hard-failing.
The setup wizard is deliberately narrow in scope:
- it generates app and legal config only
- it does not write files on the server
- it never renders or exports
DATABASE_URL - it never asks for database passwords in the browser
- it treats infrastructure as informational only
tempoll's current UX is intentionally opinionated:
- public flow: organizer creates an event, participants join with only a name, availability is edited directly on the heatmap
- organizer flow: private manage URL, no mandatory account system
- calendar: Monday-first, compact popover calendar, no special "today" highlight
- event creation: one contiguous date range, expanded internally into
dates: string[] - legal pages: opt-in
- language: English-only for now
If you are building a hosted version, these constraints are worth preserving unless you intentionally want to change the product.
The Prisma schema is intentionally small:
Event: metadata, timezone, slot size, meeting duration, organizer manage secret, statusEventDate: concrete expanded dates for the eventParticipant: participant identity, color, normalized name, edit token hashAvailabilitySlot: selected slot rows stored in UTC
- Organizer access is controlled by a private bearer-style manage URL.
- Participant editing is tied to an HTTP-only cookie scoped to that event.
- Recent event history is stored only in the browser, never synced to the server.
- Organizer URLs are stored locally on purpose and should always be treated as sensitive.
- Organizer digest emails may include a private manage link, so the recipient mailbox becomes part of that trust boundary.
Realtime updates are built on:
- PostgreSQL
LISTEN/NOTIFY - a server-side
pglistener - SSE at
/api/events/[slug]/stream
Current behavior:
- the client that saves availability applies the returned snapshot directly
- other connected clients react to the SSE event and refetch the latest snapshot
The cleanup logic in src/lib/realtime.ts matters. Closed streams must be removed carefully to avoid listener leaks or broken heartbeats.
| Route | Purpose |
|---|---|
POST /api/events |
Create a new event |
GET /api/events/[slug] |
Fetch the public event snapshot |
POST /api/events/[slug]/participants |
Join an event with a name and establish an edit session |
PUT /api/events/[slug]/availability |
Save availability for the current participant |
GET /api/events/[slug]/stream |
Subscribe to realtime event updates via SSE |
POST /api/datafast/events |
Proxy/filter optional DataFast analytics events |
PATCH /api/manage/[token] |
Update event status/title or rename a participant |
DELETE /api/manage/[token]/participants/[participantId] |
Remove a participant |
GET /api/health |
Health check that remains available before setup completes |
.
├── docker-compose.yaml
├── Dockerfile
├── prisma/
│ ├── schema.prisma
│ └── migrations/
├── public/
├── src/
│ ├── app/ # Next.js routes and API endpoints
│ ├── components/ # UI and feature components
│ ├── lib/ # domain logic, realtime, setup, tokens, utilities
│ └── test/ # test setup
├── .env.example
└── .env.coolify.example
pnpm dev
pnpm build
pnpm start
pnpm lint
pnpm typecheck
pnpm test:run
pnpm prisma:generate
pnpm prisma:migrate
pnpm prisma:studioBefore opening a PR or cutting a release, the usual minimum is:
pnpm lint
pnpm typecheck
pnpm test:runIf you touch deployment-related files, Prisma wiring, or dependency boundaries, also run:
pnpm build
docker build -t tempoll-debug .If you change the Prisma schema or Prisma-related setup, also run:
pnpm prisma:generate- Contribution policy: CONTRIBUTING.md (currently issues-first)
- Community conduct: CODE_OF_CONDUCT.md
- Security reporting: SECURITY.md
- This repo targets
Next.js 16. Do not assume older Next.js conventions. Read the relevant guide innode_modules/next/dist/docs/before making framework-level changes. - Runtime imports must live in
dependencies, not only in your local dev tree. - The Docker image intentionally copies
prisma/andprisma.config.tsinto both build and runtime stages. - The runtime container skips
pnpm prisma migrate deployunlessAPP_SETUP_COMPLETE=trueso/setupcan work on first deploys.
If you continue building a managed or hosted tempoll service, these are the current product and ops guardrails:
- Keep the product name
tempolleverywhere. - Do not reintroduce old
Terminfinderbranding in UI copy, env names, storage keys, cookies, docs, or deployment files. - Keep the public flow account-free unless you intentionally redesign it.
- Keep organizer access based on a private manage URL unless you intentionally add a real auth system.
- Keep recent-event history local-only in the browser.
- Keep private organizer links visibly marked as sensitive.
- Keep setup bootstrap non-secret. Do not render database passwords or a bundled
DATABASE_URLin the browser. - Keep legal pages opt-in via env config.
- Keep the Coolify story centered on
TEMPOLL_DB_*plusSERVICE_PASSWORD_TEMPOLL_DB, not operator-managedPOSTGRES_*. - Keep the scheduling grid compact and practical rather than turning it into oversized dashboard UI.
APP_SETUP_COMPLETE is missing or not exactly true. That is expected during first-run setup, but not after deployment is finalized.
The database volume was already initialized with older credentials. Either restore the correct password in SERVICE_PASSWORD_TEMPOLL_DB or reset the Postgres volume on purpose.
Make sure the runtime image still contains prisma.config.ts and that DATABASE_URL is available at runtime.
Check all three layers:
- Postgres
LISTEN/NOTIFY - the long-lived
pglistener insrc/lib/realtime.ts - the SSE endpoint at
/api/events/[slug]/stream
tempoll is licensed under the GNU Affero General Public License v3.0. This is intentional: tempoll is a self-hosted web app, and AGPL helps ensure that hosted or network-facing modifications remain open as well.
Copyright (c) 2026 Felix Gollnhuber.
