Configuration
TimeTiles uses three configuration layers, each suited for different types of settings:
| Layer | Format | What goes here | Changed by |
|---|---|---|---|
| Environment variables | .env | Secrets, infrastructure, paths | Operator (deploy-time) |
| Config file | config/timetiles.yml | Rate limits, quotas, batch sizes | Operator (deploy-time, restart) |
| Admin dashboard | Database (Payload CMS) | Feature flags, geocoding, branding | Admin (runtime UI) |
The config file and all environment variables (except DATABASE_URL, PAYLOAD_SECRET) are optional — sensible defaults are applied automatically.
Environment Variables
Required
DATABASE_URL=postgresql://user:password@localhost:5432/timetiles
PAYLOAD_SECRET=your-secret-key # openssl rand -base64 32Infrastructure
NODE_ENV=production # development, production, test
NEXT_PUBLIC_PAYLOAD_URL=https://your-domain.com # Required in production; dev default http://localhost:3000
NEXT_PUBLIC_SITE_URL=https://... # If different from NEXT_PUBLIC_PAYLOAD_URL
DEFAULT_LOCALE=en # en or de — BUILD-time only, see below
RATE_LIMIT_BACKEND=memory # memory or pg
DEPLOYMENT_ENVIRONMENT=production # production, staging, preview, development
RUN_AUTO_ACTIVATIONS=false # Activate configured data packages on startupSet NEXT_PUBLIC_PAYLOAD_URL to the public URL of your deployment; the localhost default is only suitable for local use.
DEPLOYMENT_ENVIRONMENT labels the instance; anything other than production shows an environment banner. RUN_AUTO_ACTIVATIONS=true activates the data packages listed in data-packages.activations.yml when the app starts; set it on one process only (the Compose file sets it on web).
Use RATE_LIMIT_BACKEND=pg for horizontally scaled web deployments. Leave it as memory for local development, tests, and explicitly single-process deployments.
DEFAULT_LOCALE is inlined into the server and client bundles when the app is built (it drives locale-prefix routing on both sides, so it cannot vary at runtime). Set it before running next build, or pass it as a Docker build arg (--build-arg DEFAULT_LOCALE=de); the published images are built with en unless the repository’s DEFAULT_LOCALE Actions variable is set. Setting it at container runtime has no effect.
File Paths
UPLOAD_DIR=uploads # Base directory (includes media/ and ingest-files/)
UPLOAD_TEMP_DIR=/tmp # Temporary files during upload
DATA_EXPORT_DIR=.exports # User data export output
INGEST_FILE_RETENTION_HOURS=24 # Keep uploaded source files this long after an import finishes
INGEST_FILE_ORPHAN_GRACE_HOURS=12 # Minimum age before an unreferenced upload is removedLogging
LOG_LEVEL=info # trace, debug, info, warn, error, fatal, silent
LOG_FILE=/var/log/timetiles/app.log # Optional file loggingEmail (SMTP)
In production the app refuses to start without EMAIL_SMTP_HOST. To run without outgoing mail, set EMAIL_DISABLED=true instead: registration, password reset and email change then answer 503, and the health check reports email as degraded. Setting both is a startup error.
Public registration is on by default, and a self-registered user cannot sign in until they have confirmed their email address. With EMAIL_DISABLED=true, also turn off Enable Public Registration in the feature flags so the sign-up form is hidden, and create accounts yourself.
In development, missing SMTP settings use an Ethereal test inbox rather than delivering messages to recipients. Tests and production builds use an in-memory JSON transport and do not send email.
EMAIL_SMTP_HOST=smtp.example.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=your-user
EMAIL_SMTP_PASS=your-password
EMAIL_FROM_ADDRESS=noreply@timetiles.io
EMAIL_FROM_NAME=TimeTiles
EMAIL_SMTP_TLS_SERVERNAME=mail.example.com # Optional: TLS name when the host is an IP
EMAIL_SMTP_REQUIRE_TLS=true # Default; false allows plaintext on a trusted relay
EMAIL_DISABLED=true # Only without EMAIL_SMTP_HOSTOn port 465 the connection uses TLS from the start. On any other port the app requires the server to offer STARTTLS and refuses to send in plaintext unless EMAIL_SMTP_REQUIRE_TLS=false.
The production Compose file also accepts EMAIL_FROM as the sender address, which the shipped .env.production template uses.
Security
SSRF_DNS_CHECK=false # Opt in to DNS validation outside production
ALLOW_PRIVATE_URLS=false # Keep private/internal URL targets blocked
PASSWORD_HIBP_CHECK=true # Reject passwords found in known breaches
TRUSTED_PROXY_CIDRS= # Comma-separated CIDRs of trusted reverse proxiesProduction always enforces DNS-based SSRF checks, even when SSRF_DNS_CHECK=false. ALLOW_PRIVATE_URLS=true is a bypass for development, tests, and the explicit E2E runtime; it does not enable private targets in normal production deployments.
PASSWORD_HIBP_CHECK is on by default and sends the first five characters of each new password’s SHA-1 hash to the Have I Been Pwned range API. Set it to false on hosts without outbound internet access or if you do not want that request.
TRUSTED_PROXY_CIDRS decides which proxies’ X-Forwarded-For entries the rate limiter trusts. It is empty by default, so forwarded client IPs are ignored in production. The production Compose file sets 172.16.0.0/12 to cover the bundled Nginx; extend it if another proxy or CDN sits in front.
Scraper Integration
Optional — only needed if running the TimeScrape runner.
SCRAPER_RUNNER_URL=http://localhost:4000 # http://host.docker.internal:4000 under Docker Compose
SCRAPER_API_KEY=your-api-key-min-16-charsConfig File (timetiles.yml)
For structured settings that don’t fit flat environment variables — rate limits with multiple time windows, per-trust-level quotas, cache tuning.
Copy the example and customize:
cp apps/web/config/timetiles.example.yml apps/web/config/timetiles.ymlThe file is optional. If absent, all defaults apply. Changes require a server restart.
In Docker, the config directory is mounted read-only over /app/apps/web/config in all four app containers, via CONFIG_DIR in your .env.production. There is one correct value, and both the bootstrap script and the shipped template use it:
CONFIG_DIR=../apps/web/configDo not change it to ./config. That path inside the image holds more than your timetiles.yml — it also ships the bundled data-package manifests (data-packages/*.yml and data-packages.activations.yml). A directory bind mount replaces the image’s directory instead of merging with it, so pointing CONFIG_DIR at a directory without those manifests removes every bundled data package. Nothing logs a warning; the feature is simply empty.
deployment/config/ contains an identical copy of timetiles.example.yml for reference, but it is not a valid mount source for the same reason.
Rate Limits
Per-endpoint rate limiting with burst, hourly, and daily windows. limit must be a positive integer; windowMs is in milliseconds.
Window identifiers must be unique within each windows list, including trust-level overrides. The identifier is name when supplied, otherwise <windowMs>ms (for example, 1000ms). Duplicate identifiers are rejected because they would share a counter instead of enforcing independent limits.
rateLimits:
FILE_UPLOAD:
windows:
- { limit: 1, windowMs: 5000, name: burst } # 1 per 5 seconds
- { limit: 5, windowMs: 3600000, name: hourly } # 5 per hour
- { limit: 20, windowMs: 86400000, name: daily } # 20 per day
API_GENERAL:
windows:
- { limit: 5, windowMs: 1000, name: burst }
- { limit: 50, windowMs: 3600000, name: hourly }
REGISTRATION:
windows:
- { limit: 3, windowMs: 60000, name: burst }
- { limit: 10, windowMs: 3600000, name: hourly }
- { limit: 20, windowMs: 86400000, name: daily }The accepted endpoint names are listed in RATE_LIMIT_ENDPOINTS in apps/web/lib/config/app-config.ts and in the commented template timetiles.example.yml. An unknown name is rejected at startup.
Quotas by Trust Level
Override the default quotas per trust level. Count quotas accept -1 for unlimited or a nonnegative integer. The file-size quota maxFileSizeMB must be a positive integer; it does not support -1 or 0. You only need to include the values you want to change.
quotas:
2: # Regular users
maxEventsPerImport: 20000
maxTotalEvents: 100000
3: # Trusted users
maxFileUploadsPerDay: 100Trust levels: 0 = Untrusted, 1 = Basic, 2 = Regular, 3 = Trusted, 4 = Power User, 5 = Unlimited. See Usage Limits for the full default table.
Trust-Level Rate Limits
Per-trust-level rate limits for file uploads and API access:
trustLevelRateLimits:
0: # Untrusted — very restrictive
FILE_UPLOAD:
windows:
- { limit: 1, windowMs: 60000, name: burst }
- { limit: 1, windowMs: 3600000, name: hourly }
API_GENERAL:
windows:
- { limit: 1, windowMs: 1000, name: burst }
- { limit: 10, windowMs: 3600000, name: hourly }Batch Sizes
Tune the import pipeline’s processing batch sizes:
batchSizes:
duplicateAnalysis: 5000
schemaDetection: 10000
eventCreation: 1000
databaseChunk: 1000Cache
URL fetch cache for scheduled imports:
cache:
urlFetch:
dir: /tmp/url-fetch-cache
maxSizeBytes: 104857600 # 100 MB
defaultTtlSeconds: 3600 # 1 hour
maxTtlSeconds: 2592000 # 30 days
respectCacheControl: true # Honor RFC 7234 headersReview Thresholds
Rates between 0 and 1 above which an import pauses for review instead of creating events:
reviewThresholds:
highDuplicateRate: 0.8 # More than 80% of rows are duplicates
geocodingPartialFailureRate: 0.5 # More than 50% of geocoding fails
highRowErrorRate: 0.1 # More than 10% of rows fail during event creation
highEmptyRowRate: 0.2 # More than 20% of rows are emptyScheduled ingests and scrapers can override these per source in the admin dashboard.
Account Settings
account:
deletionGracePeriodDays: 30Ingest
A running import task renews its lease every minute. A task silent for longer than leaseMinutes counts as abandoned: the hourly recovery check reports it, and timetiles ingest recover --apply requeues or fails it. Shorter leases notice a crash sooner; the minimum is 5.
ingest:
leaseMinutes: 15Admin Dashboard Settings
These are configured at runtime through the admin UI — no restart needed.
Feature Flags
Configure at /dashboard/globals/settings under “Feature Flags”:
| Flag | Default | Description |
|---|---|---|
| Allow Private Imports | On | Users can create private imports |
| Enable Scheduled Ingests | On | Automated URL-based imports |
| Enable Public Registration | On | Self-registration for new users |
| Enable Event Creation | On | Events can be created via import or API |
| Enable Dataset Creation | On | Users can create new datasets |
| Enable Import Creation | On | Users can start new imports |
| Enable Scheduled Job Execution | On | Scheduled imports run automatically |
| Enable URL Fetch Caching | On | Cache URL fetches to reduce external requests |
| Enable Scrapers | Off | Scraper repos and execution (trust level 3+) |
| Enable Expert Mode | Off | Clustering algorithm and parameter controls in the map UI |
Geocoding
Three free geocoding providers (Photon VersaTiles, Photon Komoot, Nominatim) are enabled by default with ~40 req/s combined throughput. No API keys required.
See the dedicated Geocoding page for provider configuration, weighted distribution, throttling behavior, location bias, and self-hosting options.
Branding & Content
- Branding (
/dashboard/globals/branding) — site name, description, logos, favicon - Main Menu (
/dashboard/globals/main-menu) — navigation links - Footer (
/dashboard/globals/footer) — tagline, social links, columns, copyright - Newsletter (
/dashboard/globals/settings) — external newsletter service URL - Legal Notices (
/dashboard/globals/settings) — terms and privacy links plus an optional disclaimer on the registration page
Roles
| Role | Permissions |
|---|---|
| User | Own catalogs, datasets, and imports |
| Editor | User + branding, advanced dataset config |
| Admin | Full access: user management, settings, feature flags |
Next Steps
- Deploy to production
- Set up backups
- Customize the UI with themes and branding
- Configure usage limits and trust levels