Skip to Content
⚠️Active Development Notice: TimeTiles is under active development. Information may be placeholder content or not up-to-date.
Self-HostingConfiguration

Configuration

TimeTiles uses three configuration layers, each suited for different types of settings:

LayerFormatWhat goes hereChanged by
Environment variables.envSecrets, infrastructure, pathsOperator (deploy-time)
Config fileconfig/timetiles.ymlRate limits, quotas, batch sizesOperator (deploy-time, restart)
Admin dashboardDatabase (Payload CMS)Feature flags, geocoding, brandingAdmin (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 32

Infrastructure

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 startup

Set 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 removed

Logging

LOG_LEVEL=info # trace, debug, info, warn, error, fatal, silent LOG_FILE=/var/log/timetiles/app.log # Optional file logging

Email (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_HOST

On 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 proxies

Production 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-chars

Config 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.yml

The 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/config

Do 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: 100

Trust 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: 1000

Cache

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 headers

Review 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 empty

Scheduled ingests and scrapers can override these per source in the admin dashboard.

Account Settings

account: deletionGracePeriodDays: 30

Ingest

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: 15

Admin Dashboard Settings

These are configured at runtime through the admin UI — no restart needed.

Feature Flags

Configure at /dashboard/globals/settings under “Feature Flags”:

FlagDefaultDescription
Allow Private ImportsOnUsers can create private imports
Enable Scheduled IngestsOnAutomated URL-based imports
Enable Public RegistrationOnSelf-registration for new users
Enable Event CreationOnEvents can be created via import or API
Enable Dataset CreationOnUsers can create new datasets
Enable Import CreationOnUsers can start new imports
Enable Scheduled Job ExecutionOnScheduled imports run automatically
Enable URL Fetch CachingOnCache URL fetches to reduce external requests
Enable ScrapersOffScraper repos and execution (trust level 3+)
Enable Expert ModeOffClustering 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

RolePermissions
UserOwn catalogs, datasets, and imports
EditorUser + branding, advanced dataset config
AdminFull access: user management, settings, feature flags

Next Steps

Last updated on