This is a Telegram bot for managing soccer players' journals.
Idea: admin (coach) makes list of questions before traning session and after it - both parts make a journal per session (day). Questions require free form answer (text). Bot allows players to participate in this quiz and saves answers to the journal.
Journals are available to read for players themselves and admin (coach) could read them for all players.
Project was created mostly to refresh Java, Spring Boot, skills. Also it is a way to learn GraalVM native image compilation, newer Java versions, Pulumi, etc.
Main technologies:
- Java 21,
- Spring Boot 3.5.6,
- PostgreSQL 18,
- GraalVM native image compilation
- Pulumi
- Google Cloud Run v2
- 2 roles: admin (aka coach) and player.
- Only one set of questions for the session could be active - it is avaialable to take by players any time.
- Admin writes questions in the format - one question per line, if line starts with "Before: " then this is a question before (and this prefix is not kept in the question), if starts with "After: " then this is question after the training session. Everything should fit into one message. For example admin runs
/questionscommand and after "Please update session questions" invite from the bot provides following message:
Before: What is your personal goal on this session?
After: Have goal been archived?
After: If yes then how?
After: If no then why?
After: What you did good during todays session?
After: What you would try to work on on next session?
- Players answer on questions one-by-one, "before or after" information only makes 2 groups of questions, "before" questions for "/before" command, "after" questions for "/after" command. Flow is - run "/before" before the session, answer on questions one-by-one, at the end get something like "Done for now, good luck with the sesssion, run
/aftercommand once you finish it." from the bot and follow this instruction after the session. - Players may see theirs journals via
/lastcommand. Each journal starts with date label, next question, colon, answer./last5command should return the last 5 journals./last50command should return the last 50 journals. - Admin may get list of players participating in journals with
/participants.
- Open Telegram and search for
@BotFather - Send
/newbotcommand - Follow the prompts to create your bot:
- Choose a name for your bot (e.g., "Journals Bot")
- Choose a username (must end with 'bot', e.g., "journals_soccer_bot")
- Save the Bot Token provided by BotFather
- Save the Bot Username (without @)
- Create
.envfile by copying.env.exampleand filling in the values:cp .env.example .env # Edit .env with your bot credentials, webhook secret, and database credentials
For development purposes project requires PostgreSQL to be installed and running.
In production project uses Supabase database.
On Ubuntu/Debian:
# Install PostgreSQL (if not already installed)
sudo apt update
sudo apt install postgresql postgresql-contrib
# Start PostgreSQL service
sudo systemctl start postgresql
sudo systemctl enable postgresql
# Check if PostgreSQL is running
sudo systemctl status postgresqlOn macOS:
# Install via Homebrew
brew install postgresql@18
brew services start postgresql@18# Create a user and database for the application
sudo -u postgres psql
# In PostgreSQL prompt, run:
CREATE DATABASE journals;
CREATE DATABASE test_journals; # For tests
# Or create user with password (optional):
CREATE USER journals_user WITH PASSWORD 'your_password';
GRANT ALL PRIVILEGES ON DATABASE journals TO journals_user;
GRANT ALL PRIVILEGES ON DATABASE test_journals TO journals_user;
# Exit PostgreSQL prompt
\qThe application uses the following defaults (can be overridden via environment variables):
- Host:
localhost - Port:
5432 - Database:
journals(for tests:test_journals) - Username:
postgres - Password:
postgres
For local development, set these in your .env file:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=journals
DB_USERNAME=postgres
DB_PASSWORD=postgresIf you don't set these in .env, the application will use the defaults (localhost).
For production deployment (Cloud Run), set these separate production database values in your .env file:
DB_PROD_HOST=your-production-db-host.example.com
DB_PROD_PORT=5432
DB_PROD_NAME=journals
DB_PROD_USERNAME=your-production-db-user
DB_PROD_PASSWORD='your-production-db-password'
DB_PROD_SSL_PARAMS='?sslmode=require'Important: If your password contains special characters (like $, &, ), etc.), use single quotes around the value to prevent shell expansion. For example:
- ✅ Correct:
DB_PROD_PASSWORD='pass)@$&word' - ❌ Wrong:
DB_PROD_PASSWORD="pass)@$&word"(double quotes allow$expansion) - ❌ Wrong:
DB_PROD_PASSWORD=pass)@$&word(no quotes will cause syntax issues)
Note: Python's Pulumi script uses override=True when loading the .env file, so it reads values directly from the file and ignores any Make-expanded environment variables.
Note: The Pulumi deployment script reads DB_PROD_* variables from your .env file and sets them as DB_* environment variables in Cloud Run.
You can keep both configurations in the same .env file - local values for development, production values for deployment.
Note: Tests automatically create the test_journals database if it doesn't exist. Just run make test and the database setup will be handled automatically.
Before deploying to Cloud Run, you can test the production database connection in two ways:
1. Test database connection only:
# Test production database connection (uses DB_PROD_* variables from .env)
make db-test-prodThis will:
- Connect to your production database (Supabase)
- Test the connection
- Show database version and tables in the public schema
2. Run the application with production database:
# Run with JDK (Spring Boot JVM mode)
make run-prod
# Or run with native image (faster, closer to production)
make native-build # Build native image first
make native-run-prodThis will:
- Start the application locally
- Connect to your production database (Supabase)
- Use the same configuration as Cloud Run
- Allow you to test the full application before deploying
Make sure your .env file has the DB_PROD_* variables set correctly before running these commands.
The easiest way to get started is using the provided Makefile:
# 1. Set up environment
cp .env.example .env
# Edit .env with your bot credentials
# 2. Start the application
make run
# Run this in one terminal
# 3. Set up webhook automatically (in another terminal)
make set-local
# This will start ngrok and set webhook automatically
# 4. Check everything is working
make health
make check-webhook- Install ngrok: https://ngrok.com/download
- Start your application:
make run # or: ./gradlew bootRun - In another terminal, expose your local server:
make ngrok # or: ngrok http 8080 - Copy the HTTPS URL from ngrok (e.g.,
https://abc123.ngrok.io) - Set the webhook URL:
NGROK_URL=https://abc123.ngrok.io make set-webhook
For production, deploy your application to Google Cloud Run v2 and set the webhook on Telegram server side to avoid abusing by third parties.
To set webhook on Telegram server side, you need to generate a secret token and add it to your .env file.
-
Generate a strong secret token:
# Generate a random secret (32 characters) openssl rand -hex 16 -
Add to your
.envfile:TELEGRAM_WEBHOOK_SECRET=your_generated_secret_here
-
The application will automatically:
- Validate the secret token on each webhook request
- Reject unauthorized requests with 401 status
- Log security violations for monitoring
Without a secret token: The webhook will be open to everyone (development mode only).
# Set production webhook (will ask for your domain URL)
make set-prodThis project uses Pulumi to deploy to Google Cloud Run v2 with GraalVM native compilation for fast startup times (~0.4 seconds).
-
Pulumi CLI: Install with one command
curl -fsSL https://get.pulumi.com | sh # Restart your shell or run: export PATH=$PATH:$HOME/.pulumi/bin
-
Python 3.8+: For Pulumi dependencies
cd pulumi python3 -m venv venv source venv/bin/activate # On Windows: venv\Scripts\activate pip install -r requirements.txt
-
Google Cloud SDK: Install and authenticate
# Install gcloud CLI curl https://sdk.cloud.google.com | bash exec -l $SHELL # Authenticate gcloud auth login gcloud auth application-default login
-
Docker: For building and pushing images
# Ubuntu/Debian sudo apt update sudo apt install docker.io sudo usermod -aG docker $USER
-
Create or select a GCP project:
gcloud projects create <project-id> gcloud config set project <project-id>
-
Enable necessary APIs:
gcloud services enable \ artifactregistry.googleapis.com \ run.googleapis.com \ iam.googleapis.com \ logging.googleapis.com \ monitoring.googleapis.com \ cloudbuild.googleapis.com \ compute.googleapis.comNote: Compute Engine API is required for Pulumi to manage Cloud Run resources properly.
-
Configure Docker to push images to Artifact Registry:
# Replace <region> with your GCP region (e.g., europe-west1) gcloud auth configure-docker <region>-docker.pkg.dev
Note: Artifact Registry is the recommended container registry for GCP. The old Google Container Registry (GCR) is deprecated.
-
Ensure your database is accessible:
- For production deployment, set
DB_PROD_HOST,DB_PROD_PORT,DB_PROD_NAME,DB_PROD_USERNAME, andDB_PROD_PASSWORDin your.envfile pointing to your production database (e.g., Supabase) - These values will be read by Pulumi and set as
DB_*environment variables in Cloud Run - For local development, use
DB_HOST,DB_PORT, etc. with localhost values - Cloud Run automatically sets
PORT=8080- your application must listen on this port (default configuration does this)
- For production deployment, set
-
Set up your
.envfile in the project root with required variables:GCP_PROJECT_ID: Your Google Cloud project IDGCP_REGION: Google Cloud region (default: europe-west1)FUNCTION_NAME: Cloud Run service name (default: tg-journals-function)TELEGRAM_BOT_TOKEN: Your Telegram bot tokenTELEGRAM_WEBHOOK_SECRET: Webhook secret for security- For local development:
DB_HOST,DB_PORT,DB_NAME,DB_USERNAME,DB_PASSWORD(use localhost) - For production:
DB_PROD_HOST,DB_PROD_PORT,DB_PROD_NAME,DB_PROD_USERNAME,DB_PROD_PASSWORD(use your production database)
-
Build Docker image locally (GraalVM native compilation takes time):
# From project root make docker-build -
Initialize Pulumi using local filesystem state (first time only):
cd pulumi source venv/bin/activate # Activate venv if not already activated mkdir -p ../.pulumi-state pulumi login file://$(pwd)/../.pulumi-state pulumi stack init dev
-
Deploy the infrastructure (uses your local Docker image):
# From project root make gcp-deploy # Or from pulumi directory (with venv activated): # cd pulumi # source venv/bin/activate # pulumi up
-
View outputs:
cd pulumi source venv/bin/activate # Activate venv if not already activated pulumi stack output
- Artifact Registry Repository: Docker repository for storing container images
- Docker Image: Pushes your locally built image to Artifact Registry
- Cloud Run v2 Service: Serverless container service with minimal resource usage
- Service Account: With minimal required permissions (logging, monitoring)
- IAM Bindings: For public access to the Cloud Run service
- HTTPS: Provided by default (no additional cost)
- Memory: 512Mi (minimum required for Cloud Run v2 with CPU allocation)
- CPU: 1 vCPU
- Scaling: Min 0 instances, Max 1 instance (scales to zero when idle)
- Request Timeout: 60 seconds
- Startup Time: ~0.4 seconds (GraalVM native image)
To destroy all resources (including Artifact Registry repository and images):
cd pulumi
source venv/bin/activate # Activate venv if not already activated
mkdir -p ../.pulumi-state
pulumi login file://$(pwd)/../.pulumi-state
pulumi destroyThis will remove:
- Cloud Run v2 service
- Artifact Registry repository and all Docker images
- Service account and IAM bindings
Note: All infrastructure is managed by Pulumi, so pulumi destroy will clean up everything.
Set Local Webhook (for development with ngrok):
make set-localSet Production Webhook (for production deployment):
make set-prodCheck Webhook Status:
make check-webhookDelete Webhook (to stop receiving updates):
make delete-webhook- Start the application:
make run - Check the health endpoint:
make health - Set the webhook URL:
make set-local(for development) ormake set-prod(for production) - Send
/startto your bot in Telegram - Check webhook status:
make check-webhook
- Health Check:
GET /health- Returns database statistics with caching - API Documentation:
GET /docs- Swagger UI - Webhook:
POST /webhook- Telegram webhook endpoint
Liquibase or Flyway are too heavy/long solutions (they check schema on startup) so keep db migrations manual for now.
When schema changes are introduced, a standalone SQL file is added to the manual_migrations directory with the date in "YYYY_MM_DD.sql" format.
How to apply a migration:
# Example for the existing migration.
psql "$DB_NAME" -h "$DB_HOST" -U "$DB_USERNAME" -p "$DB_PORT" -f manual_migrations/2025_11_04.sqlmake health # Check local application health- Add support for localization + Russian.
- Fix removing old images from GCP Artifact Registry.
- Add multiple teams.
- Send notifications to players about new sessions (to avoid spam players can just delete bot - journals will stay anyway).
- Auto-remove old journals or extend ability to view them.
- Search in team journals by text (admin only).