Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ptshen/timeful-plus/llms.txt

Use this file to discover all available pages before exploring further.

Running Timeful on localhost:3002 is fine for local evaluation, but production deployments need TLS termination, a real domain name, and correctly registered OAuth redirect URIs. This page walks through every step using either Nginx or Caddy as the reverse proxy, then covers the .env changes required to tie everything together.

System Requirements

MinimumRecommended
CPU1 vCPU2 vCPUs
RAM1 GB2 GB
Disk10 GB20 GB+
OSLinux (kernel ≥ 4.15)Ubuntu 22.04 LTS / Debian 12
Docker20.10Latest stable
Docker Composev2.0Latest stable

Reverse Proxy Configuration

The Timeful Docker stack exposes a single port — 3002 — from the frontend Nginx container. Your host-level reverse proxy forwards HTTPS traffic to that port and handles TLS certificate management.
Install Nginx on the host and obtain a TLS certificate (e.g. via Certbot):
sudo apt install nginx certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com
Create /etc/nginx/sites-available/timeful with the following content, then symlink it to sites-enabled:
server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name yourdomain.com;

    ssl_certificate     /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass         http://localhost:3002;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection 'upgrade';
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;
    }
}
sudo ln -s /etc/nginx/sites-available/timeful /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

Custom Domain Setup

After your reverse proxy is serving traffic, update the backend to match your domain by editing .env and restarting.
1

Set BASE_URL in .env

BASE_URL is used to construct OAuth callback URLs, email links, and Stripe redirect URLs. It must exactly match the origin your users reach Timeful at — including the protocol and without a trailing slash.
BASE_URL=https://yourdomain.com
2

Set CORS_ALLOWED_ORIGINS in .env

Add every origin that will send API requests to the backend. Localhost ports are always permitted automatically.
# Single domain
CORS_ALLOWED_ORIGINS=https://yourdomain.com

# Apex + www
CORS_ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com

# Multiple subdomains
CORS_ALLOWED_ORIGINS=https://timeful.corp.com,https://staging.corp.com
Always use the full URL with protocol (https://). No trailing slashes. When this variable is set, the default Timeful production domains are replaced, not appended — include every origin you need.
3

Update Google OAuth redirect URI

In Google Cloud Console → APIs & Services → Credentials, edit your OAuth 2.0 Client ID and add the following Authorised redirect URI:
https://yourdomain.com/api/auth/google/callback
Remove any stale localhost entries if this instance is public-facing only.
4

Restart the backend

Configuration changes in .env are picked up on container startup. A restart is sufficient — no rebuild is needed.
# Source build
docker compose restart backend

# Pre-built GHCR images
docker compose -f docker-compose.ghcr.yml restart backend

Complete Production .env Example

# .env — production custom domain deployment

# Required
ENCRYPTION_KEY=<output of: openssl rand -base64 32>

# Custom domain
BASE_URL=https://timeful.example.com
CORS_ALLOWED_ORIGINS=https://timeful.example.com,https://www.timeful.example.com

# Google OAuth (required for user accounts and calendar integration)
CLIENT_ID=123456789-abc.apps.googleusercontent.com
CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxxxxxx

# Microsoft OAuth (optional — Outlook calendar integration)
MICROSOFT_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
MICROSOFT_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Premium: unlock all features for all users (no Stripe required)
SELF_HOSTED_PREMIUM=true

# Email notifications (optional)
GMAIL_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx
SCHEJ_EMAIL_ADDRESS=noreply@example.com

# Slack alerts (optional)
SLACK_PROD_WEBHOOK_URL=https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXX

Security Checklist

Never commit .env or any file containing OAuth secrets to version control. Add .env and config.js to your .gitignore.
ItemRecommendation
HTTPSMandatory in production — OAuth providers refuse plain-HTTP redirect URIs
ENCRYPTION_KEYGenerate with openssl rand -base64 32; back it up securely
OAuth credentialsKeep CLIENT_SECRET and MICROSOFT_CLIENT_SECRET in .env only, never in config.js
MongoDB exposureBy default port 27017 is bound to 0.0.0.0 in Compose — restrict it to 127.0.0.1:27017 in production
Container privilegesAll containers run as non-root with no-new-privileges:true and cap_drop: ALL out of the box
Regular updatesRun make pull-ghcr weekly to pull patched images
BackupsSchedule make backup via cron; store dumps off-host
To bind MongoDB only to localhost, edit the ports mapping in your Compose file:
mongodb:
  ports:
    - "127.0.0.1:27017:27017"

Configuration Reference

Full reference for every .env variable and config.js option

Updates & Backups

Automate rolling updates and scheduled MongoDB snapshots

Security

Container hardening, secrets management, and audit logging

Troubleshooting

Diagnose OAuth errors, port conflicts, and connectivity issues

Build docs developers (and LLMs) love