An AI-powered weekly planner. Pin tasks to a backlog board; each week, a planning conversation (Telegram or web) walks you through which tasks to schedule, and turns the ones you agree to into calendar time blocks, delivered as emailed calendar invitations.
Hosted at backlog.fyi. The source is open (AGPL-3.0-only) so you can read it, audit how it handles your data, and run it yourself. See License and Contributing.
Product spec: docs/SPEC.md.
Incomplete, but functional:
- Backlog CRUD (tasks, categories, tags) with a pinboard-style React frontend.
- Passwordless auth, decoupled from Telegram — Telegram Login Widget, email magic-link, demo sandbox, and a dev-login bypass for local iteration. Multiple login methods per account ("connected accounts") via an
auth_identitiesmodel. Login no longer requires a Telegram account. - Session-cookie security, CSRF, Liquibase-managed schema.
- Prometheus metrics (
/actuator/prometheus), health probes (/actuator/health/liveness,/actuator/health/readiness), structured JSON logging via Logstash encoder. - Production config: PostgreSQL via env vars, secure session cookie, graceful shutdown.
- Weekly planning conversation.
- Google Calendar integration
- Backend: Kotlin 2.3 + Spring Boot 4.1 on JVM 25, Spring Data JPA, Spring Security 7, Liquibase.
- Database: Postgres in dev/prod, H2 for tests and in-memory dev.
- Frontend: React 19 + TypeScript + Vite 8, bundled into the backend at build time and served same-origin.
- Auth: multiple providers resolved through an
auth_identitiestable (Telegram OIDC, email magic-link, demo, dev) →HttpSessioncookie (SameSite=Lax,HttpOnly,Securein prod).TaskerPrincipalcarries onlyuserId, so the session layer is provider-agnostic. Prometheus scraper uses HTTP Basic Auth on a separate stateless filter chain. Seedocs/archive/AUTH-DECOUPLING.md. - Observability: Micrometer + Prometheus registry; health probes for liveness/readiness; structured JSON log rotation via Logstash encoder (prod profile).
- JDK 25
- Node 20+ and npm (for the frontend build)
- Docker (for the Postgres container, started automatically via
spring-boot-docker-compose)
./gradlew bootRunWith environment variables:
export $(cat .env | xargs) && ./gradlew bootRunThat's the whole flow in dev:
- Gradle runs
npm ciandnpm run buildfor the frontend. - The built frontend is copied into
src/main/resources/static/. - The
bootRunGradle task passes--spring.profiles.active=dev, so the app uses the H2 in-memory database.spring-boot-docker-composealso starts Postgres fromcompose.yaml, but the dev profile does not use it. - Spring Boot serves the SPA and the API on
http://localhost:8080.
Open http://localhost:8080. Click Dev login (skip Telegram) to sign in as the deterministic dev user — this works under the dev profile without any BotFather setup.
If you want Vite HMR, from tasker-frontend/:
npm run devYou'll need a dev proxy to forward /api/** to :8080 (not set up by default; the bundled-into-backend flow is the primary dev loop today).
Telegram is no longer required to sign in — email magic-link, the demo sandbox, and (in dev) the dev-login button all work without it. Set this up when you want the Telegram login button (an OAuth2/OIDC redirect, Telegram Login docs) and the Telegram-driven weekly planning conversation.
/newbotwith @BotFather, save the token.- In BotFather → Bot Settings → Web Login, set the HTTPS host you'll serve from (Telegram won't redirect back to bare
http://localhost) and copy the Client ID and Client Secret. - Export:
export TASKER_TELEGRAM_CLIENT_ID=... # backend: OIDC client id (bot id) + expected id_token aud export TASKER_TELEGRAM_CLIENT_SECRET=... # backend: OIDC client secret (token-exchange credential) export TASKER_TELEGRAM_BOT_TOKEN=... # backend: bot messaging (planning conversation) export TASKER_TELEGRAM_BOT_USERNAME=... # backend (cosmetic)
- Restart
./gradlew bootRun. The login page's Telegram button kicks off the OAuth redirect; without the Client ID/Secret it shows a "temporarily unavailable" notice and you fall back to the dev-login button.
Without these vars, the dev-login button (and the demo sandbox) get you in for local work; email magic-link login also works once email is configured.
src/main/kotlin/dev/itayp/tasker/
config/ Spring config (security, Prometheus auth, dev seed, telegram properties, time)
controller/ REST controllers (auth, tasks, categories, tags)
service/ Business logic (TelegramOidcService, UserAuthService, …)
security/ TaskerPrincipal + SessionAuthenticator
jpa/ JPA entities
repository/ Spring Data repositories
model/ API DTOs (requests, responses, domain enums)
src/main/resources/
application.yaml base config (shared across all profiles)
application-dev.yaml H2 datasource
application-prod.yaml PostgreSQL datasource + secure cookie (env-var driven)
logback-spring.xml plain console in dev; rolling JSON files in prod
db/changelog/ Liquibase master + changesets
tasker-frontend/src/
auth/ AuthProvider, LoginPage, auth API client
components/ PostItNote, TaskDrawer, SettingsModal
App.tsx, api.ts, types.ts, …
docs/SPEC.md Product spec
compose.yaml Postgres for local dev (started automatically, not used by dev profile)
Backend:
./gradlew bootRun # run the app
./gradlew build # compile + test + bundle frontend
./gradlew test # all tests
./gradlew test --tests "dev.itayp.tasker.SomeTest.method" # single testFrontend (from tasker-frontend/):
npm run dev # Vite dev server
npm run build # tsc -b && vite build (also runs via Gradle)
npm run lint
npm run test # vitest runThe app is a self-contained fat JAR deployed as a systemd service. Start it with the prod profile active:
SPRING_PROFILES_ACTIVE=prod java -jar taskboard.jarRequired environment variables:
| Variable | Purpose |
|---|---|
TASKER_APP_BASE_URL |
Public URL of the instance, e.g. https://tasks.example.com (no trailing slash) |
TASKER_DB_URL |
JDBC URL, e.g. jdbc:postgresql://localhost:5432/taskboard |
TASKER_DB_USERNAME |
Postgres user |
TASKER_DB_PASSWORD |
Postgres password |
TASKER_DATA_KEK |
base64-encoded 32-byte key wrapping per-user DEKs for at-rest encryption. Losing it loses all encrypted data. Generate with openssl rand -base64 32 |
Startup fails fast if the base URL, database or data-encryption key is missing. Everything else is optional and degrades instead: Telegram (login needs TASKER_TELEGRAM_CLIENT_ID/_SECRET, the bot needs TASKER_TELEGRAM_BOT_TOKEN), AI (TASKER_AI_API_KEY), metrics (TASKER_PROMETHEUS_USERNAME/_PASSWORD; the endpoint is closed without them) and email (below). To sign in without SMTP or Telegram, list users in TASKER_LOCAL_USERS; TASKER_REGISTRATION=closed and TASKER_DEMO_ENABLED=false keep strangers out. The startup log summarizes what's on. Full list in docs/CONFIGURATION.md.
Email (optional, for login magic links and calendar invites) is split into two independent SMTP senders — auth (login/register/verification) and scheduling (calendar invites). Enable with TASKER_EMAIL_ENABLED=true and set the TASKER_EMAIL_AUTH_* / TASKER_EMAIL_SCHEDULING_* variables (from address + SMTP host/port/username/password per sender). Full list in docs/CONFIGURATION.md. When disabled, both senders drop messages (logging only that they did) and the login page hides email sign-in.
| Endpoint | Auth | Purpose |
|---|---|---|
/actuator/health |
public | Overall health (DB connectivity etc.) |
/actuator/health/liveness |
public | Liveness probe |
/actuator/health/readiness |
public | Readiness probe |
/actuator/prometheus |
Basic Auth (PROMETHEUS role) |
Prometheus scrape target |
Backend:
- Unit tests — Mockito + JUnit 5 for services (
BacklogTaskServiceTest,TelegramOidcServiceTest,UserAuthServiceTest). - Controller slice tests —
@WebMvcTest+SecurityConfigurationso auth + CSRF behavior is exercised (AuthControllerTest,BacklogTaskControllerTest). - Integration tests —
@SpringBootTest(RANDOM_PORT)withTestRestTemplatefor real session-cookie reuse, CSRF enforcement, and prod-profile gating (SecurityIntegrationTest). - Postgres integration tests —
AbstractIntegrationTest.Initializerstarts a TestContainersPostgreSQLContainerand wires it into the Spring context. Tests run under@ActiveProfiles("prod")and exercise JPA and the health endpoints against a real database (PostgresIntegrationTest,SecurityIntegrationProdProfileTest).
All time-dependent code takes an injected Clock, so tests can use Clock.fixed(...) and assert deterministic behavior.
For specific tests:
./gradlew --info test --tests dev.itayp.tasker.channel.email.EmailIntegrationTestFrontend (tasker-frontend/):
- Vitest + React Testing Library +
@testing-library/jest-dom, jsdom environment. Config lives invite.config.ts'stestfield; the setup file issrc/setupTests.ts. Test files are co-located next to the file under test asFoo.test.tsx— seesrc/NotFoundPage.test.tsxfor the baseline shape. Coverage is minimal so far; add tests alongside new/changed code as you go.
npm run test # vitest runEvery pull request to main runs .github/workflows/gradle.yml:
frontendjob —npm ci,npm run lint,npm run test,npm run build, all intasker-frontend/.buildjob —./gradlew build(compiles, runs backend tests, bundles the frontend into the JAR).dependency-submissionjob — submits the Gradle dependency graph for Dependabot alerts.
Possible, but not turnkey yet: you need Postgres, a Telegram bot (optional for web-only use), an OpenRouter key for the AI features, and SMTP accounts for email. See "Production deployment" above and docs/CONFIGURATION.md. Easier self-hosting (a Docker Compose setup, a self-hosting guide) is tracked in issue #271.
Copyright (C) 2026 Itay Polack-Gadassi.
Code: GNU Affero General Public License v3.0 only (AGPL-3.0-only). If you run a modified version as a network service, the AGPL requires you to offer its users the corresponding source.
Mascot artwork: CC BY 4.0. The "Backlog.fyi" name, logo and icons are reserved: fine to keep when self-hosting (modified or not), but a public service needs its own logo and icons and must not pose as the official one; see TRADEMARKS.md. Third-party components (fonts, the disposable-domain list) and details: NOTICE.md.
This is a solo project and pull requests are not accepted. Bug reports are welcome; see CONTRIBUTING.md for details.