From b87e5dd023781feb84fe1fd1455f9ecee1b99fb5 Mon Sep 17 00:00:00 2001 From: Amruth Pillai Date: Tue, 20 Jan 2026 10:35:37 +0100 Subject: [PATCH] add more guides on migration and self-hosting docker compose examples, change launch date for banner, update dependencies --- deployment/postgres/Dockerfile | 12 - deployment/postgres/init.sql | 46 -- deployment/postgres/postgresql.conf | 122 ---- deployment/swarm/compose.yml | 206 ------- docs/docs.json | 4 +- .../docker.mdx} | 3 +- docs/self-hosting/examples.mdx | 567 ++++++++++++++++++ docs/self-hosting/migration.mdx | 305 ++++++++++ package.json | 4 +- pnpm-lock.yaml | 42 +- .../_home/-sections/product-hunt-banner.tsx | 6 +- 11 files changed, 901 insertions(+), 416 deletions(-) delete mode 100644 deployment/postgres/Dockerfile delete mode 100644 deployment/postgres/init.sql delete mode 100644 deployment/postgres/postgresql.conf delete mode 100644 deployment/swarm/compose.yml rename docs/{guides/self-hosting-with-docker.mdx => self-hosting/docker.mdx} (99%) create mode 100644 docs/self-hosting/examples.mdx create mode 100644 docs/self-hosting/migration.mdx diff --git a/deployment/postgres/Dockerfile b/deployment/postgres/Dockerfile deleted file mode 100644 index 6caadd1ef..000000000 --- a/deployment/postgres/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM postgres:latest - -COPY ./postgresql.conf /etc/postgresql/postgresql.conf - -COPY ./init.sql /docker-entrypoint-initdb.d/ - -RUN chown postgres:postgres /etc/postgresql/postgresql.conf && \ - chmod 644 /etc/postgresql/postgresql.conf - -EXPOSE 5432 - -CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"] \ No newline at end of file diff --git a/deployment/postgres/init.sql b/deployment/postgres/init.sql deleted file mode 100644 index 09bd44636..000000000 --- a/deployment/postgres/init.sql +++ /dev/null @@ -1,46 +0,0 @@ --- Enable pg_stat_statements extension -CREATE EXTENSION IF NOT EXISTS pg_stat_statements; - --- Enable other useful extensions -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- UUID generation -CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- Cryptographic functions -CREATE EXTENSION IF NOT EXISTS "btree_gin"; -- GIN indexes for btree types -CREATE EXTENSION IF NOT EXISTS "btree_gist"; -- GIST indexes for btree types - --- Create a function to reset pg_stat_statements (useful for debugging) -CREATE OR REPLACE FUNCTION reset_query_stats() -RETURNS void AS $$ -BEGIN - PERFORM pg_stat_statements_reset(); -END; -$$ LANGUAGE plpgsql; - --- Create a view for easier query analysis -CREATE OR REPLACE VIEW slow_queries AS -SELECT - query, - calls, - ROUND(total_exec_time::numeric, 2) AS total_time_ms, - ROUND(mean_exec_time::numeric, 2) AS mean_time_ms, - ROUND(max_exec_time::numeric, 2) AS max_time_ms, - ROUND((100 * total_exec_time / SUM(total_exec_time) OVER ())::numeric, 2) AS percentage, - rows -FROM pg_stat_statements -ORDER BY total_exec_time DESC; - --- Create a view for most frequent queries -CREATE OR REPLACE VIEW frequent_queries AS -SELECT - query, - calls, - ROUND(mean_exec_time::numeric, 2) AS mean_time_ms, - ROUND(total_exec_time::numeric, 2) AS total_time_ms, - rows -FROM pg_stat_statements -ORDER BY calls DESC; - --- Notification for successful initialization -DO $$ -BEGIN - RAISE NOTICE 'Database initialized with pg_stat_statements and helper views'; -END $$; \ No newline at end of file diff --git a/deployment/postgres/postgresql.conf b/deployment/postgres/postgresql.conf deleted file mode 100644 index df4021b2a..000000000 --- a/deployment/postgres/postgresql.conf +++ /dev/null @@ -1,122 +0,0 @@ -# ----------------------------- -# PostgreSQL 18 configuration file -# Optimized for: 8GB RAM, 6 vCPU, KVM Virtual Machine -# ----------------------------- - -# CONNECTIONS AND AUTHENTICATION -listen_addresses = '*' -max_connections = 100 -superuser_reserved_connections = 3 - -# MEMORY SETTINGS -shared_buffers = 2GB -huge_pages = try -effective_cache_size = 6GB -maintenance_work_mem = 512MB -work_mem = 20MB - -# QUERY TUNING -random_page_cost = 1.1 -effective_io_concurrency = 200 -default_statistics_target = 100 - -# WRITE AHEAD LOG (WAL) -wal_level = replica -wal_buffers = 16MB -min_wal_size = 1GB -max_wal_size = 4GB -wal_compression = on -checkpoint_completion_target = 0.9 -checkpoint_timeout = 15min - -# BACKGROUND WRITER -bgwriter_delay = 200ms -bgwriter_lru_maxpages = 100 -bgwriter_lru_multiplier = 2.0 - -# AUTOVACUUM -autovacuum = on -autovacuum_max_workers = 3 -autovacuum_naptime = 1min -autovacuum_vacuum_threshold = 50 -autovacuum_vacuum_scale_factor = 0.1 -autovacuum_analyze_threshold = 50 -autovacuum_analyze_scale_factor = 0.05 -autovacuum_vacuum_cost_delay = 2ms -autovacuum_vacuum_cost_limit = 200 - -# MONITORING AND STATISTICS -track_activities = on -track_counts = on -track_io_timing = on -track_functions = all -track_wal_io_timing = on -compute_query_id = on - -# pg_stat_statements configuration -pg_stat_statements.track = all -pg_stat_statements.max = 10000 -pg_stat_statements.track_utility = on -pg_stat_statements.save = on - -# LOGGING -log_destination = 'stderr' -logging_collector = on -log_directory = 'log' -log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log' -log_file_mode = 0600 -log_rotation_age = 1d -log_rotation_size = 100MB -log_truncate_on_rotation = on - -# What to log -log_min_duration_statement = 1000 -log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h ' -log_checkpoints = on -log_connections = on -log_disconnections = on -log_duration = off -log_lock_waits = on -log_statement = 'none' -log_temp_files = 0 -log_timezone = 'UTC' - -# LOCALE AND FORMATTING -datestyle = 'iso, mdy' -timezone = 'UTC' -lc_messages = 'en_US.utf8' -lc_monetary = 'en_US.utf8' -lc_numeric = 'en_US.utf8' -lc_time = 'en_US.utf8' -default_text_search_config = 'pg_catalog.english' - -# LOCK MANAGEMENT -deadlock_timeout = 1s -max_locks_per_transaction = 64 -max_pred_locks_per_transaction = 64 - -# CLIENT CONNECTION DEFAULTS -search_path = '"$user", public' -idle_in_transaction_session_timeout = 600000 -statement_timeout = 0 - -# PARALLEL QUERY EXECUTION -max_worker_processes = 6 -max_parallel_workers_per_gather = 3 -max_parallel_workers = 6 -max_parallel_maintenance_workers = 3 -parallel_leader_participation = on - -# INCREMENTAL BACKUP -summarize_wal = on - -# VACUUM OPTIMIZATION -vacuum_buffer_usage_limit = 256kB - -# VECTOR EXTENSION SETTINGS -maintenance_work_mem = 512MB -max_parallel_maintenance_workers = 3 -work_mem = 32MB - -# EXTENSIONS -shared_preload_libraries = 'pg_stat_statements' \ No newline at end of file diff --git a/deployment/swarm/compose.yml b/deployment/swarm/compose.yml deleted file mode 100644 index 4018cd648..000000000 --- a/deployment/swarm/compose.yml +++ /dev/null @@ -1,206 +0,0 @@ -services: - postgres: - image: postgres:18 - command: postgres -c config_file=/etc/postgresql.conf - networks: - - reactive_resume_network - volumes: - - reactive_resume_postgres_data:/var/lib/postgresql - environment: - - POSTGRES_DB=$POSTGRES_DB - - POSTGRES_USER=$POSTGRES_USER - - POSTGRES_PASSWORD=$POSTGRES_PASSWORD - configs: - - source: reactive_resume_postgres_config - target: /etc/postgresql.conf - healthcheck: - test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s - logging: - driver: json-file - options: - max-size: "10m" - max-file: "3" - deploy: - mode: replicated - replicas: 1 - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - - gotenberg: - image: gotenberg/gotenberg:8 - networks: - - reactive_resume_network - environment: - - WEBHOOK_DISABLE=true - - CHROMIUM_AUTO_START=true - - API_ENABLE_BASIC_AUTH=true - - PROMETHEUS_DISABLE_COLLECT=true - - GOTENBERG_API_BASIC_AUTH_USERNAME=$GOTENBERG_USERNAME - - GOTENBERG_API_BASIC_AUTH_PASSWORD=$GOTENBERG_PASSWORD - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 30s - logging: - driver: json-file - options: - max-size: "10m" - max-file: "3" - deploy: - mode: replicated - replicas: 2 - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - - seaweedfs: - image: chrislusf/seaweedfs:latest - command: server -s3 -filer -dir=/data -ip=0.0.0.0 - networks: - - reactive_resume_network - volumes: - - reactive_resume_seaweedfs_data:/data - environment: - - AWS_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID - - AWS_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY - healthcheck: - test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8888"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 30s - logging: - driver: json-file - options: - max-size: "10m" - max-file: "3" - deploy: - mode: replicated - replicas: 1 - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - - seaweedfs_create_bucket: - image: quay.io/minio/mc:latest - entrypoint: > - /bin/sh -c " - until mc alias set seaweedfs http://seaweedfs:8333 $S3_ACCESS_KEY_ID $S3_SECRET_ACCESS_KEY; do - echo 'Waiting for SeaweedFS...'; - sleep 2; - done; - mc mb seaweedfs/$S3_BUCKET --ignore-existing; - " - networks: - - reactive_resume_network - deploy: - mode: replicated - replicas: 1 - restart_policy: - condition: on-failure - delay: 10s - max_attempts: 5 - window: 120s - - app: - image: ghcr.io/amruthpillai/reactive-resume-test:latest - networks: - - traefik_network - - reactive_resume_network - volumes: - - reactive_resume_app_data:/app/data - environment: - - APP_URL=$APP_URL - - GOTENBERG_ENDPOINT=$GOTENBERG_ENDPOINT - - GOTENBERG_USERNAME=$GOTENBERG_USERNAME - - GOTENBERG_PASSWORD=$GOTENBERG_PASSWORD - - DATABASE_URL=$DATABASE_URL - - AUTH_SECRET=$AUTH_SECRET - - GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID - - GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET - - GITHUB_CLIENT_ID=$GITHUB_CLIENT_ID - - GITHUB_CLIENT_SECRET=$GITHUB_CLIENT_SECRET - - SMTP_HOST=$SMTP_HOST - - SMTP_PORT=$SMTP_PORT - - SMTP_USER=$SMTP_USER - - SMTP_PASS=$SMTP_PASS - - SMTP_FROM=$SMTP_FROM - - SMTP_SECURE=$SMTP_SECURE - - S3_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID - - S3_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY - - S3_REGION=$S3_REGION - - S3_ENDPOINT=$S3_ENDPOINT - - S3_BUCKET=$S3_BUCKET - healthcheck: - test: - [ - "CMD", - "node", - "-e", - "fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))", - ] - interval: 30s - timeout: 10s - retries: 3 - start_period: 30s - logging: - driver: json-file - options: - max-size: "10m" - max-file: "3" - deploy: - mode: replicated - replicas: 2 - restart_policy: - condition: on-failure - delay: 5s - max_attempts: 3 - window: 120s - update_config: - parallelism: 1 - delay: 10s - failure_action: rollback - order: start-first - rollback_config: - parallelism: 1 - delay: 10s - labels: - - "traefik.enable=true" - - "traefik.http.routers.app.rule=Host(`rxresu.me`)" - - "traefik.http.routers.app.entrypoints=websecure" - - "traefik.http.routers.app.tls=true" - - "traefik.http.services.app.loadbalancer.server.port=3000" - -configs: - reactive_resume_postgres_config: - name: reactive_resume_postgres_config - external: true - -networks: - traefik_network: - external: true - reactive_resume_network: - name: reactive_resume_network - driver: overlay - attachable: true - -volumes: - reactive_resume_postgres_data: - name: reactive_resume_postgres_data - reactive_resume_seaweedfs_data: - name: reactive_resume_seaweedfs_data - reactive_resume_app_data: - name: reactive_resume_app_data diff --git a/docs/docs.json b/docs/docs.json index 24eab9bd4..7833cb168 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -66,8 +66,8 @@ "pages": ["guides/using-the-api", "guides/using-ai", "guides/json-resume-schema"] }, { - "group": "Self-hosting", - "pages": ["guides/self-hosting-with-docker"] + "group": "Self-Hosting", + "pages": ["self-hosting/docker", "self-hosting/examples", "self-hosting/migration"] }, { "group": "Contributing", diff --git a/docs/guides/self-hosting-with-docker.mdx b/docs/self-hosting/docker.mdx similarity index 99% rename from docs/guides/self-hosting-with-docker.mdx rename to docs/self-hosting/docker.mdx index ee0d57e4e..6261b7387 100644 --- a/docs/guides/self-hosting-with-docker.mdx +++ b/docs/self-hosting/docker.mdx @@ -1,5 +1,5 @@ --- -title: "Self-hosting with Docker" +title: "Self-Hosting with Docker" description: "A comprehensive guide to self-host Reactive Resume with Docker (Postgres + Gotenberg), including a detailed environment variable reference and troubleshooting tips." --- @@ -461,4 +461,3 @@ A healthy response returns HTTP 200. Any other response (or a connection failure - **Fix**: Set `S3_FORCE_PATH_STYLE="true"` in your environment. This is required for most self-hosted S3-compatible services like MinIO, SeaweedFS, etc. - diff --git a/docs/self-hosting/examples.mdx b/docs/self-hosting/examples.mdx new file mode 100644 index 000000000..632feab85 --- /dev/null +++ b/docs/self-hosting/examples.mdx @@ -0,0 +1,567 @@ +--- +title: "Docker Compose Examples" +description: "A collection of Docker Compose examples for different deployment scenarios. If you have a different setup that works for you, please share it by opening a pull request on GitHub." +--- + +## Overview + +Every self-hosted setup is unique. You might be running on a single VPS, a Kubernetes cluster, behind Cloudflare Tunnel, or using a specific reverse proxy like Traefik or nginx. This page provides real-world Docker Compose configurations for various deployment scenarios to help you get started faster. + +These examples go beyond the basic setup in the [Self-Hosting with Docker](/self-hosting/docker) guide, showing production-ready configurations with reverse proxies, SSL termination, and other common patterns. + + + **Help others by sharing your setup!** If you have a working configuration that isn't covered here, I'd love to include it. Simply [open a pull request](https://github.com/amruthpillai/reactive-resume) with your example added to this page. Your contribution helps the community and makes self-hosting easier for everyone. + + +--- + +## Docker with Traefik + +This example uses [Traefik](https://traefik.io/) as a reverse proxy with automatic SSL certificate management via Let's Encrypt. Only the Reactive Resume app is exposed through Traefikβ€”Postgres and Gotenberg remain on an internal network. + + + Traefik automatically discovers services via Docker labels and handles SSL certificates, making it ideal for setups where you want minimal configuration. + + +```yaml compose-traefik.yml lines expandable +services: + traefik: + image: traefik:v3.2 + restart: unless-stopped + command: + - "--api.dashboard=true" + - "--providers.docker=true" + - "--providers.docker.exposedbydefault=false" + - "--entrypoints.web.address=:80" + - "--entrypoints.websecure.address=:443" + - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true" + - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web" + - "--certificatesresolvers.letsencrypt.acme.email=${ACME_EMAIL}" + - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json" + - "--entrypoints.web.http.redirections.entryPoint.to=websecure" + - "--entrypoints.web.http.redirections.entryPoint.scheme=https" + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - traefik_letsencrypt:/letsencrypt + networks: + - reactive_resume_network + labels: + - "traefik.enable=true" + # Dashboard (optional, remove if not needed) + - "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)" + - "traefik.http.routers.traefik.entrypoints=websecure" + - "traefik.http.routers.traefik.tls.certresolver=letsencrypt" + - "traefik.http.routers.traefik.service=api@internal" + - "traefik.http.routers.traefik.middlewares=auth" + - "traefik.http.middlewares.auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}" + + postgres: + image: postgres:16 + restart: unless-stopped + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - reactive_resume_network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + interval: 10s + timeout: 5s + retries: 5 + + gotenberg: + image: gotenberg/gotenberg:8 + restart: unless-stopped + command: + - "gotenberg" + - "--chromium-auto-start" + - "--api-timeout=120s" + networks: + - reactive_resume_network + extra_hosts: + - "host.docker.internal:host-gateway" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + + app: + image: amruthpillai/reactive-resume:latest + restart: unless-stopped + environment: + - APP_URL=https://resume.${DOMAIN} + - PRINTER_APP_URL=http://app:3000 + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres + - GOTENBERG_ENDPOINT=http://gotenberg:3000 + - AUTH_SECRET=${AUTH_SECRET} + # Add other optional env vars as needed (SMTP, S3, OAuth, etc.) + volumes: + - app_data:/app/data + networks: + - reactive_resume_network + depends_on: + postgres: + condition: service_healthy + gotenberg: + condition: service_healthy + labels: + - "traefik.enable=true" + - "traefik.http.routers.reactive-resume.rule=Host(`resume.${DOMAIN}`)" + - "traefik.http.routers.reactive-resume.entrypoints=websecure" + - "traefik.http.routers.reactive-resume.tls.certresolver=letsencrypt" + - "traefik.http.services.reactive-resume.loadbalancer.server.port=3000" + healthcheck: + test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\""] + interval: 30s + timeout: 10s + retries: 3 + +networks: + reactive_resume_network: + driver: bridge + +volumes: + traefik_letsencrypt: + postgres_data: + app_data: +``` + +**Environment variables (`.env`):** + +```bash .env +DOMAIN="example.com" +ACME_EMAIL="admin@example.com" +POSTGRES_PASSWORD="your-secure-postgres-password" +AUTH_SECRET="your-auth-secret-from-openssl-rand-hex-32" +# Optional: Traefik dashboard auth (generate with: htpasswd -nb admin password) +TRAEFIK_DASHBOARD_AUTH="admin:$$apr1$$..." +``` + +--- + +## Docker with nginx + +This example uses [nginx](https://nginx.org/) as a reverse proxy with SSL certificates (you'll need to provide your own certificates or use certbot separately). + +```yaml compose-nginx.yml lines expandable +services: + nginx: + image: nginx:alpine + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx.conf:/etc/nginx/nginx.conf:ro + - ./certs:/etc/nginx/certs:ro + networks: + - reactive_resume_network + depends_on: + - app + + postgres: + image: postgres:16 + restart: unless-stopped + environment: + POSTGRES_DB: postgres + POSTGRES_USER: postgres + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - reactive_resume_network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + interval: 10s + timeout: 5s + retries: 5 + + gotenberg: + image: gotenberg/gotenberg:8 + restart: unless-stopped + command: + - "gotenberg" + - "--chromium-auto-start" + - "--api-timeout=120s" + networks: + - reactive_resume_network + extra_hosts: + - "host.docker.internal:host-gateway" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + + app: + image: amruthpillai/reactive-resume:latest + restart: unless-stopped + environment: + - APP_URL=https://resume.${DOMAIN} + - PRINTER_APP_URL=http://app:3000 + - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@postgres:5432/postgres + - GOTENBERG_ENDPOINT=http://gotenberg:3000 + - AUTH_SECRET=${AUTH_SECRET} + # Add other optional env vars as needed (SMTP, S3, OAuth, etc.) + volumes: + - app_data:/app/data + networks: + - reactive_resume_network + depends_on: + postgres: + condition: service_healthy + gotenberg: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "node -e \"fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\""] + interval: 30s + timeout: 10s + retries: 3 + +networks: + reactive_resume_network: + driver: bridge + +volumes: + postgres_data: + app_data: +``` + +**nginx configuration (`nginx.conf`):** + +```nginx nginx.conf lines expandable +events { + worker_connections 1024; +} + +http { + upstream reactive_resume { + server app:3000; + } + + # Redirect HTTP to HTTPS + server { + listen 80; + server_name _; + return 301 https://$host$request_uri; + } + + # HTTPS server + server { + listen 443 ssl http2; + server_name resume.example.com; + + ssl_certificate /etc/nginx/certs/fullchain.pem; + ssl_certificate_key /etc/nginx/certs/privkey.pem; + + # SSL configuration + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + # Proxy settings + location / { + proxy_pass http://reactive_resume; + 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; + + # Timeouts for long-running requests (PDF generation) + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + } + + # Increase max body size for resume uploads + client_max_body_size 10M; + } +} +``` + + + For automatic SSL certificates with nginx, consider using [certbot](https://certbot.eff.org/) with the `--nginx` plugin, or a companion container like [nginx-proxy-acme](https://github.com/nginx-proxy/acme-companion). + + +--- + +## Docker Swarm + +This example demonstrates a production-grade Docker Swarm deployment with multiple replicas, health checks, rolling updates, and Traefik integration. It includes SeaweedFS for S3-compatible storage and a PostgreSQL database with custom configuration. + + + Docker Swarm is great for multi-node deployments where you need high availability and easy scaling. The app service is configured with 2 replicas and rolling update strategy. + + +```yaml compose-swarm.yml lines expandable +services: + postgres: + image: postgres:18 + command: postgres -c config_file=/etc/postgresql.conf + networks: + - reactive_resume_network + volumes: + - reactive_resume_postgres_data:/var/lib/postgresql + environment: + - POSTGRES_DB=$POSTGRES_DB + - POSTGRES_USER=$POSTGRES_USER + - POSTGRES_PASSWORD=$POSTGRES_PASSWORD + configs: + - source: reactive_resume_postgres_config + target: /etc/postgresql.conf + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + deploy: + mode: replicated + replicas: 1 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + + gotenberg: + image: gotenberg/gotenberg:8 + networks: + - reactive_resume_network + environment: + - WEBHOOK_DISABLE=true + - CHROMIUM_AUTO_START=true + - API_ENABLE_BASIC_AUTH=true + - PROMETHEUS_DISABLE_COLLECT=true + - GOTENBERG_API_BASIC_AUTH_USERNAME=$GOTENBERG_USERNAME + - GOTENBERG_API_BASIC_AUTH_PASSWORD=$GOTENBERG_PASSWORD + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + deploy: + mode: replicated + replicas: 2 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + + seaweedfs: + image: chrislusf/seaweedfs:latest + command: server -s3 -filer -dir=/data -ip=0.0.0.0 + networks: + - reactive_resume_network + volumes: + - reactive_resume_seaweedfs_data:/data + environment: + - AWS_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY + healthcheck: + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost:8888"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + deploy: + mode: replicated + replicas: 1 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + + seaweedfs_create_bucket: + image: quay.io/minio/mc:latest + entrypoint: > + /bin/sh -c " + until mc alias set seaweedfs http://seaweedfs:8333 $S3_ACCESS_KEY_ID $S3_SECRET_ACCESS_KEY; do + echo 'Waiting for SeaweedFS...'; + sleep 2; + done; + mc mb seaweedfs/$S3_BUCKET --ignore-existing; + " + networks: + - reactive_resume_network + deploy: + mode: replicated + replicas: 1 + restart_policy: + condition: on-failure + delay: 10s + max_attempts: 5 + window: 120s + + app: + image: ghcr.io/amruthpillai/reactive-resume:latest + networks: + - traefik_network + - reactive_resume_network + volumes: + - reactive_resume_app_data:/app/data + environment: + - APP_URL=$APP_URL + - GOTENBERG_ENDPOINT=$GOTENBERG_ENDPOINT + - GOTENBERG_USERNAME=$GOTENBERG_USERNAME + - GOTENBERG_PASSWORD=$GOTENBERG_PASSWORD + - DATABASE_URL=$DATABASE_URL + - AUTH_SECRET=$AUTH_SECRET + - GOOGLE_CLIENT_ID=$GOOGLE_CLIENT_ID + - GOOGLE_CLIENT_SECRET=$GOOGLE_CLIENT_SECRET + - GITHUB_CLIENT_ID=$GITHUB_CLIENT_ID + - GITHUB_CLIENT_SECRET=$GITHUB_CLIENT_SECRET + - SMTP_HOST=$SMTP_HOST + - SMTP_PORT=$SMTP_PORT + - SMTP_USER=$SMTP_USER + - SMTP_PASS=$SMTP_PASS + - SMTP_FROM=$SMTP_FROM + - SMTP_SECURE=$SMTP_SECURE + - S3_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID + - S3_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY + - S3_REGION=$S3_REGION + - S3_ENDPOINT=$S3_ENDPOINT + - S3_BUCKET=$S3_BUCKET + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://localhost:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))", + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + deploy: + mode: replicated + replicas: 2 + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 3 + window: 120s + update_config: + parallelism: 1 + delay: 10s + failure_action: rollback + order: start-first + rollback_config: + parallelism: 1 + delay: 10s + labels: + - "traefik.enable=true" + - "traefik.http.routers.app.rule=Host(`rxresu.me`)" + - "traefik.http.routers.app.entrypoints=websecure" + - "traefik.http.routers.app.tls=true" + - "traefik.http.services.app.loadbalancer.server.port=3000" + +configs: + reactive_resume_postgres_config: + name: reactive_resume_postgres_config + external: true + +networks: + traefik_network: + external: true + reactive_resume_network: + name: reactive_resume_network + driver: overlay + attachable: true + +volumes: + reactive_resume_postgres_data: + name: reactive_resume_postgres_data + reactive_resume_seaweedfs_data: + name: reactive_resume_seaweedfs_data + reactive_resume_app_data: + name: reactive_resume_app_data +``` + +**Deploy the stack:** + +```bash +docker stack deploy -c compose-swarm.yml reactive_resume +``` + +**Useful commands:** + +```bash +# Check service status +docker stack services reactive_resume + +# View logs for the app +docker service logs -f reactive_resume_app + +# Scale the app +docker service scale reactive_resume_app=3 + +# Remove the stack +docker stack rm reactive_resume +``` + + + This example assumes you have an external Traefik network already set up. Adjust the `traefik_network` reference and labels based on your Traefik configuration. + + +--- + +## Contributing Your Setup + +Have a different deployment setup that works well? Consider contributing it here. Some examples include: + +- Kubernetes / Helm charts +- Cloudflare Tunnel +- Caddy reverse proxy +- Docker with Portainer +- Podman configurations +- Cloud-specific deployments (AWS ECS, Google Cloud Run, Azure Container Apps) + +To contribute, [open a pull request](https://github.com/amruthpillai/reactive-resume) with your example added to this page. Include: + +1. A brief description of when/why someone would use this setup +2. The complete Docker Compose (or equivalent) configuration +3. Any additional configuration files (nginx.conf, etc.) +4. Required environment variables \ No newline at end of file diff --git a/docs/self-hosting/migration.mdx b/docs/self-hosting/migration.mdx new file mode 100644 index 000000000..eb918d2e4 --- /dev/null +++ b/docs/self-hosting/migration.mdx @@ -0,0 +1,305 @@ +--- +title: "Migrating from v4 to v5" +description: "A step-by-step guide to migrate your Reactive Resume instance from v4 to v5, including manual and automated migration options." +--- + +## Overview + +This guide walks you through migrating your Reactive Resume installation from **v4 to v5**. The migration process involves setting up a new v5 instance alongside your existing v4 instance, then transferring your users and resumes to the new system. + + + **Keep your v4 instance running** until you have successfully migrated all data to v5 and verified everything works correctly. This ensures you have a fallback in case anything goes wrong during the migration. + + +## Prerequisites + +Before starting the migration, ensure you have: + + + + Your existing Reactive Resume v4 instance should be running and accessible. + + + A fresh Reactive Resume v5 instance set up and running. Follow the [Self-Hosting with Docker](/self-hosting/docker) guide if you haven't done this yet. + + + Access to both your v4 PostgreSQL database (source) and v5 PostgreSQL database (target). + + + A recent backup of your v4 database. Always backup before any migration. + + + +## Choosing a Migration Method + +The best migration approach depends on the size of your instance: + + + + **Best for**: Small instances with a handful of resumes. + + Uses the built-in Import Dialog to manually convert resumes one at a time. + + + **Best for**: Large instances with many users and resumes. + + Uses migration scripts to batch-process all users and resumes automatically. + + + +## Manual Migration (Small Instances) + +If you have only a few resumes to migrate, the simplest approach is to use the **Import Dialog** feature in v5. + + + + In your v4 instance, go to each resume and export it as JSON. This creates a portable file containing all your resume data. + + + + In your new v5 instance: + 1. Log in or create a new account + 2. Click **Create Resume** or use the **Import** option + 3. Select the **Reactive Resume v4** format + 4. Upload your exported JSON file + + The import process automatically converts the v4 format to v5. + + + + Review the imported resume to ensure all data transferred correctly. Repeat for each resume you need to migrate. + + + + + The Import Dialog handles the schema conversion automatically, so you don't need to worry about format differences between v4 and v5. + + +## Automated Migration (Large Instances) + +For instances with many users and resumes, use the migration scripts to automate the process. The migration happens in two phases: first users, then resumes. + +### Requirements + +To run the migration scripts, you need the following installed on your host machine: + + + + **tsx** - TypeScript execution environment. Install globally with: + ```bash + npm install -g tsx + ``` + + + **dotenvx** (or any tool to load `.env` files). Install globally with: + ```bash + npm install -g @dotenvx/dotenvx + ``` + Alternatively, you can use `dotenv`, `direnv`, or export the variables manually. + + + Clone the Reactive Resume repository to access the migration scripts: + ```bash + git clone https://github.com/amruthpillai/reactive-resume.git + cd reactive-resume + pnpm install + ``` + + + +### Environment Setup + +Create a `.env` file in the root of the repository with the following variables: + +```bash .env +# Connection string to your NEW v5 PostgreSQL database (target) +DATABASE_URL="postgresql://user:password@localhost:5432/reactive_resume_v5" + +# Connection string to your OLD v4 PostgreSQL database (source) +PRODUCTION_DATABASE_URL="postgresql://user:password@localhost:5432/reactive_resume_v4" +``` + + + Double-check your connection strings! `DATABASE_URL` should point to your **new v5 database** and `PRODUCTION_DATABASE_URL` should point to your **old v4 database**. Mixing these up could cause data loss. + + +### Step 1: Migrate Users + +The user migration script transfers all user accounts, authentication data, and two-factor settings from v4 to v5. + +```bash +dotenvx run -- tsx scripts/migration/user.ts +``` + +**What this script does:** + +- Fetches users in batches from the v4 database +- Creates corresponding user accounts in the v5 database +- Migrates authentication providers (email, Google, GitHub, custom OAuth) +- Preserves two-factor authentication settings and backup codes +- Creates a mapping file (`scripts/migration/user-id-map.json`) that links old user IDs to new ones + + + The script saves progress automatically. If interrupted (Ctrl+C), you can run it again and it will resume from where it left off. + + +**Expected output:** + +``` +βŒ› Starting user migration... +πŸ“₯ Fetching users batch from production database (OFFSET 0)... +πŸ“‹ Found 1000 users in this batch. +πŸ“ Preparing to bulk insert 1000 users... +βœ… Bulk inserted 1000 users in 245.3 ms (avg 0.2 ms/user) +πŸ’Ύ Progress saved at offset 1000 +πŸ“¦ Processed 1000 users so far... + +πŸ“Š Migration Summary: + Users created: 1000 + Accounts created: 1000 + Two-factor entries created: 50 + Skipped (already exist): 0 +⏱️ Total migration time: 1234.5 ms (1.23 seconds) +βœ… User migration complete! +``` + +### Step 2: Migrate Resumes + +After users are migrated, run the resume migration script. This script depends on the user ID mapping created in the previous step. + +```bash +dotenvx run -- tsx scripts/migration/resume.ts +``` + +**What this script does:** + +- Fetches resumes in batches from the v4 database +- Converts each resume from v4 format to v5 format automatically +- Links resumes to the correct users using the ID mapping +- Migrates resume statistics (views, downloads) +- Preserves visibility settings (public/private) and lock status + + + Like the user script, the resume migration also saves progress and can be resumed if interrupted. + + +**Expected output:** + +``` +βŒ› Starting resume migration... +πŸ“₯ Fetching resumes batch from production database (OFFSET 0)... +πŸ“‹ Found 2500 resumes in this batch. +πŸ“ Preparing to bulk insert 2500 resumes... +βœ… Bulk inserted 2500 resumes in 892.1 ms (avg 0.4 ms/resume) +πŸ’Ύ Progress saved at offset 2500 +πŸ“¦ Processed 2500 resumes so far... + +πŸ“Š Migration Summary: + Resumes created: 2500 + Statistics created: 2500 + Skipped (userId not found or already exist): 0 + Errors: 0 +⏱️ Total migration time: 5678.9 ms (5.68 seconds) +βœ… Resume migration complete! +``` + +### Progress and Recovery + +Both migration scripts support graceful shutdown and resume: + +- **Progress files**: `scripts/migration/user-progress.json` and `scripts/migration/resume-progress.json` track the current migration state +- **User ID mapping**: `scripts/migration/user-id-map.json` maps v4 user IDs to v5 user IDs +- **Graceful shutdown**: Press `Ctrl+C` to stop the migration safely. Progress is saved before exit. +- **Resume migration**: Run the script again to continue from where you left off + + + If you need to restart the migration from scratch, delete the progress files and the user ID mapping file before running the scripts again. + + +## Post-Migration Steps + +After completing the migration: + + + + Log into your v5 instance and spot-check several user accounts and resumes to ensure data transferred correctly. + + + + - Create a test resume and export it as PDF + - Verify social logins work (if configured) + - Check that two-factor authentication works for migrated users + + + + Once verified, update your DNS records or reverse proxy to point to the new v5 instance. + + + + After confirming everything works and allowing a grace period, you can safely shut down your v4 instance. + + + +## Important Notes + + + + Users who signed up with email/password can continue using their existing passwords. No password reset is required after migration. + + + + User profile pictures (avatars) are stored as references in the database. If you were using S3 storage, ensure your v5 instance has access to the same bucket, or users may need to re-upload their avatars. + + + + Similar to profile pictures, any images embedded in resumes need to be accessible from your v5 instance. Consider migrating your storage bucket or updating references as needed. + + + + If you're using custom OAuth providers, ensure the same providers are configured in v5 with matching client IDs. Users authenticate with the same provider ID, so mismatched configurations will cause login failures. + + + + The v5 schema has some changes from v4: + - `visibility` (public/private) is now `isPublic` (boolean) + - Resume `title` is now `name` + - Some resume data fields have been reorganized + + The migration scripts handle these conversions automatically. + + + +## Troubleshooting + + + + Ensure your `.env` file contains both `DATABASE_URL` and `PRODUCTION_DATABASE_URL`, and that you're using a tool like `dotenvx` to load them before running the script. + + + + Users are skipped if: + - Their email already exists in the v5 database + - Their username already exists in the v5 database + - They were already migrated in a previous run + + Check the console output for skip reasons. + + + + Resumes are skipped if: + - The associated user wasn't migrated (user ID not in mapping file) + - A resume with the same slug already exists for that user + - They were already migrated in a previous run + + + + If a resume can't be parsed from v4 format, it will be created with default empty data. Check the console output for warnings about specific resumes, and consider manually importing those using the Import Dialog. + + + + The scripts process data in batches to avoid overwhelming the database. For very large instances: + - Consider running the migration during off-peak hours + - Ensure both databases have adequate resources + - The batch size can be adjusted in the script files if needed + + diff --git a/package.json b/package.json index 95e59dd6a..6718b7587 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@tanstack/react-query": "5.90.19", "@tanstack/react-router": "^1.153.2", "@tanstack/react-router-ssr-query": "^1.153.2", - "@tanstack/react-start": "^1.153.2", + "@tanstack/react-start": "^1.154.0", "@tanstack/zod-adapter": "^1.153.2", "@tiptap/extension-highlight": "^3.15.3", "@tiptap/extension-table": "^3.15.3", @@ -65,7 +65,7 @@ "@tiptap/pm": "^3.15.3", "@tiptap/react": "^3.15.3", "@tiptap/starter-kit": "^3.15.3", - "ai": "^6.0.41", + "ai": "^6.0.42", "ai-sdk-ollama": "^3.1.1", "bcrypt": "^6.0.0", "better-auth": "^1.5.0-beta.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e53584fcf..e0c050a75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -93,8 +93,8 @@ importers: specifier: ^1.153.2 version: 1.153.2(@tanstack/query-core@5.90.19)(@tanstack/react-query@5.90.19(react@19.2.3))(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@tanstack/router-core@1.153.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start': - specifier: ^1.153.2 - version: 1.153.2(crossws@0.4.1(srvx@0.10.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown-vite@7.3.1(@types/node@25.0.9)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)) + specifier: ^1.154.0 + version: 1.154.0(crossws@0.4.1(srvx@0.10.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown-vite@7.3.1(@types/node@25.0.9)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)) '@tanstack/zod-adapter': specifier: ^1.153.2 version: 1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(zod@4.3.5) @@ -117,11 +117,11 @@ importers: specifier: ^3.15.3 version: 3.15.3 ai: - specifier: ^6.0.41 - version: 6.0.41(zod@4.3.5) + specifier: ^6.0.42 + version: 6.0.42(zod@4.3.5) ai-sdk-ollama: specifier: ^3.1.1 - version: 3.1.1(ai@6.0.41(zod@4.3.5))(zod@4.3.5) + version: 3.1.1(ai@6.0.42(zod@4.3.5))(zod@4.3.5) bcrypt: specifier: ^6.0.0 version: 6.0.0 @@ -321,8 +321,8 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 - '@ai-sdk/gateway@3.0.16': - resolution: {integrity: sha512-OOY5CfRJiHvh/8np2vs1RQaCZ5hWv2qOeEmmeiABXK3gLQHUVnCO+1hhoLsZdHM5iElu6M407dAOfyvTsKJqcQ==} + '@ai-sdk/gateway@3.0.17': + resolution: {integrity: sha512-mCz50GlBPyBV96Wcll1Mpaz56MVFuHL+bwRWGkIsCJwKAKIfdgUZecFzS3gckpHGaqP5+BYnmyJocIMzUhTQ2A==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -3781,8 +3781,8 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-start@1.153.2': - resolution: {integrity: sha512-akQPVjSJoki5A8X/pDUpfxdcRvkTAO2d/rwStWRT6aVglmODbaNaGg8Asd9JSs6qhT2E+lKeTIZp8OSUxC1UOA==} + '@tanstack/react-start@1.154.0': + resolution: {integrity: sha512-umfxigl+oqW2oWlIYEk+FI85a9GUKvUIMLN2AH6MrGvuVqPbL5uFOl4qHY78VKTyzdDjzxndtMwoHSodm038aw==} engines: {node: '>=22.12.0'} peerDependencies: react: '>=18.0.0 || >=19.0.0' @@ -3843,8 +3843,8 @@ packages: resolution: {integrity: sha512-/zWBnfsOwact936Bn0CxigudU1QRZdiNTsK7ME/LMXXA66XsDxkryX5+5FeGwU5ETNPfLAx6pRUet1mtUKnLCg==} engines: {node: '>=22.12.0'} - '@tanstack/start-plugin-core@1.153.2': - resolution: {integrity: sha512-4r/muO6im3YZfUgoT/Ibkmb8xgNOqs7rzxTrH9pO+iCwrl9Z/3wU9N8/v97DFmYqcA3+r9/WDHC7IXazEdBCDw==} + '@tanstack/start-plugin-core@1.154.0': + resolution: {integrity: sha512-qlywkB43bltkS9poMtAEjxPG9Y4YTP4GSaCOyINYdkrfCEU42xCpUghvANFMpeKEQn+I2YvBtUlGcRpJxKyjDw==} engines: {node: '>=22.12.0'} peerDependencies: vite: '>=7.0.0' @@ -4175,8 +4175,8 @@ packages: peerDependencies: ai: ^6.0.27 - ai@6.0.41: - resolution: {integrity: sha512-FaFKBiqOX3RKEz7PJ/K6d9+1JnHilzR4ID2elRzZ/yVGC5vM4RvVRyYHlhFso/gyQRXrQZtOSbqbBchAwcTDXg==} + ai@6.0.42: + resolution: {integrity: sha512-o+MVN7HBE4HEnhtN7nBt9WO1iISI6svyWNoOuY6WiXCdHuZfSGN4MUQ3QwjWz1Ue5gtBEcvwX5XFhgAwlPAxxw==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -7573,7 +7573,7 @@ snapshots: '@ai-sdk/provider-utils': 4.0.8(zod@4.3.5) zod: 4.3.5 - '@ai-sdk/gateway@3.0.16(zod@4.3.5)': + '@ai-sdk/gateway@3.0.17(zod@4.3.5)': dependencies: '@ai-sdk/provider': 3.0.4 '@ai-sdk/provider-utils': 4.0.8(zod@4.3.5) @@ -11571,14 +11571,14 @@ snapshots: transitivePeerDependencies: - crossws - '@tanstack/react-start@1.153.2(crossws@0.4.1(srvx@0.10.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown-vite@7.3.1(@types/node@25.0.9)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0))': + '@tanstack/react-start@1.154.0(crossws@0.4.1(srvx@0.10.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rolldown-vite@7.3.1(@types/node@25.0.9)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0))': dependencies: '@tanstack/react-router': 1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start-client': 1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/react-start-server': 1.153.2(crossws@0.4.1(srvx@0.10.0))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@tanstack/router-utils': 1.143.11 '@tanstack/start-client-core': 1.153.2 - '@tanstack/start-plugin-core': 1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.1(srvx@0.10.0))(rolldown-vite@7.3.1(@types/node@25.0.9)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)) + '@tanstack/start-plugin-core': 1.154.0(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.1(srvx@0.10.0))(rolldown-vite@7.3.1(@types/node@25.0.9)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)) '@tanstack/start-server-core': 1.153.2(crossws@0.4.1(srvx@0.10.0)) pathe: 2.0.3 react: 19.2.3 @@ -11671,7 +11671,7 @@ snapshots: '@tanstack/start-fn-stubs@1.151.3': {} - '@tanstack/start-plugin-core@1.153.2(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.1(srvx@0.10.0))(rolldown-vite@7.3.1(@types/node@25.0.9)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0))': + '@tanstack/start-plugin-core@1.154.0(@tanstack/react-router@1.153.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(crossws@0.4.1(srvx@0.10.0))(rolldown-vite@7.3.1(@types/node@25.0.9)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0))': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.6 @@ -12072,18 +12072,18 @@ snapshots: agent-base@7.1.4: {} - ai-sdk-ollama@3.1.1(ai@6.0.41(zod@4.3.5))(zod@4.3.5): + ai-sdk-ollama@3.1.1(ai@6.0.42(zod@4.3.5))(zod@4.3.5): dependencies: '@ai-sdk/provider': 3.0.4 '@ai-sdk/provider-utils': 4.0.8(zod@4.3.5) - ai: 6.0.41(zod@4.3.5) + ai: 6.0.42(zod@4.3.5) ollama: 0.6.3 transitivePeerDependencies: - zod - ai@6.0.41(zod@4.3.5): + ai@6.0.42(zod@4.3.5): dependencies: - '@ai-sdk/gateway': 3.0.16(zod@4.3.5) + '@ai-sdk/gateway': 3.0.17(zod@4.3.5) '@ai-sdk/provider': 3.0.4 '@ai-sdk/provider-utils': 4.0.8(zod@4.3.5) '@opentelemetry/api': 1.9.0 diff --git a/src/routes/_home/-sections/product-hunt-banner.tsx b/src/routes/_home/-sections/product-hunt-banner.tsx index f1105f9c3..ae84cf31b 100644 --- a/src/routes/_home/-sections/product-hunt-banner.tsx +++ b/src/routes/_home/-sections/product-hunt-banner.tsx @@ -2,8 +2,8 @@ import { Trans } from "@lingui/react/macro"; import { HeartIcon } from "@phosphor-icons/react"; import { useEffect, useState } from "react"; -const PH_LAUNCH_START = Date.UTC(2026, 0, 22, 8, 1, 0); -const PH_LAUNCH_END = Date.UTC(2026, 0, 23, 8, 1, 0); +const PH_LAUNCH_START = Date.UTC(2026, 0, 21, 8, 1, 0); +const PH_LAUNCH_END = Date.UTC(2026, 0, 22, 8, 1, 0); function isWithinProductHuntLaunchWindow() { const nowUtc = Date.now(); @@ -29,8 +29,8 @@ export function ProductHuntBanner() { Reactive Resume is launching on Product Hunt today, head over to show some love!