Skip to content

Conversation

@dpolishuk
Copy link

This pull request introduces a comprehensive, production-ready deployment setup for the project, along with a technical implementation plan for incremental message sync. The most significant changes are the addition of a robust deploy.sh script for interactive and reliable Docker-based deployments, a new docker-compose.yml for orchestrating services, and documentation for implementing incremental sync in the messages API. Additionally, the Docker build now includes the Prisma schema directory.

Deployment & Infrastructure

  • Added a full-featured deploy.sh script that interactively configures environment variables, manages Docker Compose services, handles database initialization/reset, configures HTTPS with Caddy, and verifies service health for easy local or production deployment.
  • Introduced a new docker-compose.yml defining services for the app, PostgreSQL (with initialization), Redis, Minio (S3-compatible storage), and Caddy, including healthchecks, dependencies, and persistent volumes for robust orchestration.
  • Updated the Dockerfile to copy the prisma directory into the image, ensuring database schema access at runtime.

Documentation & Planning

  • Added a detailed implementation plan (docs/plans/2025-01-01-incremental-message-sync-implementation.md) for supporting incremental message synchronization in the API, including query parameter handling, Prisma indexing, and manual testing steps.

dpolishuk and others added 11 commits December 31, 2025 03:03
Adds docker-compose.yml with pre-built image support:
- app: pulls from docker.korshakov.com/handy-server
- postgres: PostgreSQL 16
- redis: Redis 7 with persistence
- minio: S3-compatible storage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Adds minio-init service that creates 'happy' bucket on startup
- Sets bucket to allow anonymous downloads
- App now waits for minio-init to complete before starting
- Adds healthcheck to minio service

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Changed docker-compose to build locally instead of pulling from registry
- Added prisma folder to Dockerfile runner stage for migrations
- Changed metrics port from 9090 to 9091 to avoid conflicts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Two-part approach to reduce iOS app load time from 2-7s to <500ms:

Part A - Incremental Sync:
- Add updatedAfter/before query params to messages API
- Track lastSyncTimestamp per session on iOS
- Fetch only new/edited messages (typically 0-5 vs 150)

Part B - Prefetch on App Active:
- Prefetch messages for top 5 active sessions when app opens
- Timeout protection (3s sessions, 5s prefetch total)
- Data ready before user navigates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Defines interactive deployment script that:
- Auto-detects environment (system vs Docker Caddy)
- Handles existing data with user prompts
- Configures PostgreSQL password securely
- Runs full health verification after deployment

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements deploy.sh with:
- Auto-detection of system vs Docker Caddy
- Interactive prompts for domain and credentials
- PostgreSQL password configuration
- Full health verification after deployment
- Idempotent - safe to run multiple times

Usage: ./deploy.sh

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds docker/init-postgres.sh that runs on postgres container
initialization to ensure the password matches the expected value.
Also enables remote debug logging and mounts logs volume.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds a postgres-init sidecar container that runs ALTER USER to ensure
the PostgreSQL password matches the expected value before the app starts.
This prevents the recurring database authentication failures.

The app now depends on postgres-init completing successfully.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Port 5432 was exposed to the public internet, causing constant
brute-force auth attempts from bots. Binding to 127.0.0.1 restricts
database access to local connections only while still allowing
debugging from the host machine.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use data.newestTimestamp instead of Date.now() to avoid clock skew
- Add "Known Limitations" section documenting:
  - Deleted messages handling (not in scope)
  - Clock skew protection approach

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Detailed task-by-task plan for adding updatedAfter/before params
to the messages endpoint and database index.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Copilot AI review requested due to automatic review settings January 2, 2026 16:05
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request introduces production deployment infrastructure and documentation for an incremental message synchronization feature. The changes include a comprehensive deployment script for Docker-based orchestration, service configuration, and detailed technical planning documents for implementing efficient message syncing.

  • Adds automated deployment tooling with interactive configuration for domains, passwords, and data handling
  • Introduces Docker Compose orchestration for the application stack with PostgreSQL, Redis, MinIO, and optional Caddy
  • Documents incremental sync architecture to reduce message fetch latency from seconds to milliseconds

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
deploy.sh Interactive deployment script handling Docker services, database setup, Caddy configuration, and health verification
docker-compose.yml Orchestrates app, PostgreSQL, Redis, MinIO, and related initialization services with healthchecks and volumes
Dockerfile Adds prisma directory to runtime image for database schema access
docs/plans/2026-01-01-deploy-script-design.md Design documentation for the deployment script architecture and workflow
docs/plans/2025-01-01-message-sync-latency-design.md Technical design for reducing message sync latency via incremental fetching
docs/plans/2025-01-01-incremental-message-sync-implementation.md Step-by-step implementation plan for the incremental sync feature

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- "3005:3005"
- "9091:9090"
environment:
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/handy
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default password "postgres" is hardcoded in the DATABASE_URL. This means even if the postgres service is configured with a different password through POSTGRES_PASSWORD, the app will still try to connect with "postgres" as the password. This configuration should reference the environment variable or be consistent with the password setup.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +20
- S3_ACCESS_KEY=minioadmin
- S3_SECRET_KEY=minioadmin
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded credentials "minioadmin/minioadmin" for S3_ACCESS_KEY and S3_SECRET_KEY are default MinIO credentials. These should be configurable and generated securely, similar to the PostgreSQL password handling in the deploy script.

Copilot uses AI. Check for mistakes.
Choice [1]: _
```

Credentials stored in `.env` file (gitignored).
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PostgreSQL password stored in the .env file is in plaintext. While the file is gitignored, consider adding a note in the documentation about securing this file with appropriate file permissions (e.g., chmod 600 .env) to prevent unauthorized access on shared systems.

Suggested change
Credentials stored in `.env` file (gitignored).
Credentials stored in `.env` file (gitignored); ensure it is protected with restrictive permissions (for example, `chmod 600 .env`) to prevent unauthorized access on shared systems.

Copilot uses AI. Check for mistakes.
})),
hasMore: messages.length === limit,
...(messages.length > 0 ? {
oldestTimestamp: Math.min(...messages.map(m => m.createdAt.getTime())),
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newestTimestamp calculation uses updatedAt while oldestTimestamp uses createdAt. This inconsistency could cause issues when the client uses newestTimestamp for the next updatedAfter query, as it's comparing different timestamp fields. Both should likely use the same field (probably updatedAt for consistency with the incremental sync feature).

Suggested change
oldestTimestamp: Math.min(...messages.map(m => m.createdAt.getTime())),
oldestTimestamp: Math.min(...messages.map(m => m.updatedAt.getTime())),

Copilot uses AI. Check for mistakes.
environment:
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/handy
- REDIS_URL=redis://redis:6379
- HANDY_MASTER_SECRET=your-super-secret-key-change-in-production
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded value "your-super-secret-key-change-in-production" for HANDY_MASTER_SECRET is insecure. This secret is used directly in the docker-compose file without any prompting or generation during deployment. Anyone with access to the repository can see this default value, creating a significant security vulnerability.

Suggested change
- HANDY_MASTER_SECRET=your-super-secret-key-change-in-production
- HANDY_MASTER_SECRET=${HANDY_MASTER_SECRET:?HANDY_MASTER_SECRET not set}

Copilot uses AI. Check for mistakes.

# Fix PostgreSQL password
info "Configuring PostgreSQL password..."
$COMPOSE_CMD exec -T postgres psql -U postgres -c "ALTER USER postgres WITH PASSWORD '$POSTGRES_PASSWORD';" > /dev/null
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PostgreSQL password is interpolated directly into the shell command without proper escaping. If the password contains special characters (like $, `, ", ), the ALTER USER command could fail or behave unexpectedly. Consider using proper quoting or escaping for the password value.

Suggested change
$COMPOSE_CMD exec -T postgres psql -U postgres -c "ALTER USER postgres WITH PASSWORD '$POSTGRES_PASSWORD';" > /dev/null
ESCAPED_POSTGRES_PASSWORD=$(printf "%s" "$POSTGRES_PASSWORD" | sed "s/'/''/g")
$COMPOSE_CMD exec -T postgres psql -U postgres -c "ALTER USER postgres WITH PASSWORD '${ESCAPED_POSTGRES_PASSWORD}';" > /dev/null

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +32
updatedAfter: z.coerce.number().int().min(0).optional(),
before: z.coerce.number().int().min(0).optional(),
limit: z.coerce.number().int().min(1).max(150).default(150)
})
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The querystring schema field should be marked as optional at the schema level using .optional(), but the inner object fields (updatedAfter, before, limit) are already optional. This creates a discrepancy where the entire querystring object is required but its contents are optional. Consider making the querystring itself optional or ensuring all fields have appropriate defaults.

Suggested change
updatedAfter: z.coerce.number().int().min(0).optional(),
before: z.coerce.number().int().min(0).optional(),
limit: z.coerce.number().int().min(1).max(150).default(150)
})
updatedAfter: z.coerce.number().int().min(0),
before: z.coerce.number().int().min(0),
limit: z.coerce.number().int().min(1).max(150)
}).partial().default({ limit: 150 }).optional()

Copilot uses AI. Check for mistakes.
- S3_ACCESS_KEY=minioadmin
- S3_SECRET_KEY=minioadmin
- S3_BUCKET=happy
- S3_PUBLIC_URL=http://localhost:9000/happy
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The S3_PUBLIC_URL uses "localhost" which will not work correctly in production or when accessed from outside the Docker network. This should be configurable based on the domain provided during deployment, similar to how the Caddy configuration uses the domain variable.

Suggested change
- S3_PUBLIC_URL=http://localhost:9000/happy
- S3_PUBLIC_URL=http://${DOMAIN:-localhost}:9000/happy

Copilot uses AI. Check for mistakes.
- S3_SECRET_KEY=minioadmin
- S3_BUCKET=happy
- S3_PUBLIC_URL=http://localhost:9000/happy
- DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING=true
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING to true in a production deployment configuration is concerning. The variable name itself indicates this should not be enabled in production. This should either be removed or set to false by default, with an option to enable it only for development environments.

Suggested change
- DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING=true
- DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING=false

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +83
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=minioadmin
Copy link

Copilot AI Jan 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same default MinIO credentials "minioadmin/minioadmin" are used here. These should match whatever credentials are configured for the app service and should be securely generated rather than using defaults.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant