A shared calendar app for running one calendar across multiple people — families, teams, talent agencies. It blends TimeTree's shared-calendar simplicity with a Google Calendar–style permission model. Monorepo: a Go REST API plus a React SPA.
Stack: Go 1.26 · Huma v2 · MySQL 8.4 · React 19 · Bun
| Monthly view | Weekly view |
|---|---|
![]() |
![]() |
| Share & embed | |
![]() |
Responsive mobile layout (vertical month scroll):
- Shared calendars — per-member roles (owner / manager / editor / viewer), invite links, public share tokens, and an embeddable (embed) view
- Events — all-day & timed, recurrence (RRULE) with per-occurrence exception editing, drag to move/resize, attendees, checklists, attachments, comments
- Memos — per-calendar notes
- Albums — per-calendar photo albums (optionally linked to events)
- Activity & audit history — change history for events/memos plus a per-calendar activity feed
- Auth — email + password, Google / LINE OAuth/OIDC, password reset, and a development-only password-less login
- Admin — OAuth provider configuration and an allowlist of permitted sign-in emails/domains
- Export / import — iCal and CSV
- i18n & themes — English / Japanese locales and theme switching
| Area | Technology |
|---|---|
| API | Go 1.26 + Huma v2 (OpenAPI) + chi/v5 + sqlc |
| DB | MySQL 8.4 (Docker Compose) |
| Object storage | S3-compatible (MinIO locally) |
| Web | React 19 + TypeScript + Vite + TanStack Router + Zustand + Tailwind CSS 4 + Luxon |
| Lint / format | Biome (web) / gofmt (Go) |
| Package manager | Bun |
| Testing | Go E2E (testify + real MySQL) / Vitest (web) |
apps/
api/ Go REST API
cmd/api/ entrypoint
cmd/createuser/ user-creation CLI
cmd/healthcheck/ container health probe
internal/ config, auth, audit, cleanup, db/generated,
errors, mailer, recurrence, secrets, storage,
http/{handlers,middleware,router}
tests/e2e/ E2E tests
web/ React SPA
src/components/ UI components
src/routes/ TanStack Router (file-based; incl. share/ and embed/)
src/stores/ Zustand (auth / calendar / ui stores)
src/lib/ utilities (date, recurrence, upload, theme, …)
src/i18n/ English / Japanese locales
sql/
core/ vendored shared schema contract (pinned; not edited here)
tables/ shared table definitions
triggers/ shared triggers
conformance/ contract conformance checks
time/ this application's own tables
tables/ table definitions
alters/ incremental changes
queries/ sqlc query definitions
schema.sql combined schema produced by build-schema.sh
sqlc.yaml sqlc config
compose.yml local stack: MySQL + MinIO
compose.prod.yml self-hosting stack: MySQL + MinIO + API + web
bun install # install deps (Bun required)
make db-up # start MySQL (compose.yml)
make db-apply # build & apply the schema
make db-seed-users # create demo@example.com / admin@example.com
make dev # start DB + API + web together- API: http://localhost:8080 (
/health; OpenAPI is generated by Huma) - Web: http://localhost:5173
In development (TC_ENV=development), /auth/dev-login provides password-less
login for the seeded accounts.
make dev # DB + API + web in parallel
make db-up / db-down # start / stop MySQL
make db-apply # apply schema (build-schema.sh → schema.sql)
make db-seed # schema + users + sample data
make minio-up # start MinIO (S3-compatible storage)
make sqlc # generate sqlc code
make api / make web # run individually
make build-api # build the API binary (bin/api)
make create-user ARGS="-email a@b.com -password secret123 -admin"
make format # gofmt + Biome autofix
make lint # gofmt check + Biome (no writes)
make test-api # Go unit tests
make test-e2e # E2E (requires MySQL + TC_TEST_INTEGRATION=1)
make test-e2e-storage # E2E including MinIO-backed storage tests
bun run check # Biome check
bun run typecheck # TypeScript typecheck (tsc -b)
cd apps/web && bun run test # web unit tests (Vitest)All prefixed with TC_. Key ones below; see apps/api/internal/config/config.go
for the full list and defaults.
| Variable | Purpose | Default |
|---|---|---|
TC_ENV |
development enables dev conveniences |
production |
TC_PORT |
API port | 8080 |
TC_DB_DSN |
MySQL DSN | ttuser:ttpw@tcp(127.0.0.1:33306)/timetree_clone?parseTime=true |
TC_JWT_SECRET |
JWT signing key (≥32 bytes required in prod) | dev fallback |
TC_PASSWORD_LOGIN_ENABLED |
enable email+password auth | true |
TC_CORS_ALLOWED_ORIGINS |
allowed CORS origins (no wildcard in prod) | localhost:5173 |
TC_TRUSTED_PROXIES |
reverse-proxy hops allowed to set X-Forwarded-For (CIDR or bare IP, comma-separated). Unset behind a proxy collapses every client onto one rate-limit bucket |
none trusted |
TC_AUTH_RATE_LIMIT |
per-IP requests/min for sign-in, registration, password reset and OAuth | 60 |
TC_SHARE_RATE_LIMIT |
per-IP requests/min for public share links, counted separately from sign-in | 600 |
TC_WEB_URL / TC_API_PUBLIC_URL |
public URLs for web / API | localhost |
TC_S3_ENDPOINT / TC_S3_ACCESS_KEY / TC_S3_SECRET_KEY / TC_S3_BUCKET / TC_S3_USE_SSL |
S3-compatible storage | MinIO defaults |
TC_SMTP_HOST and other TC_SMTP_* |
mail delivery (stdout when unset; required in prod) | console output |
TC_GOOGLE_CLIENT_ID / TC_GOOGLE_CLIENT_SECRET |
Google OAuth | empty |
TC_GOOGLE_ALLOWED_DOMAINS |
allowed Google sign-in domains (comma-separated) | unrestricted |
TC_LINE_CLIENT_ID / TC_LINE_CLIENT_SECRET |
LINE OAuth | empty |
TC_SECRETS_KEY |
key encrypting secrets stored in the DB (hex/base64, 32 bytes) | empty |
In production,
Config.Validate()refuses to start with the default JWT secret, default MinIO credentials, wildcard CORS, or an unset SMTP host.
DB / storage (compose.yml): TC_DB_PORT (default 33306), TC_DB_ROOT_PASSWORD,
TC_DB_NAME, TC_DB_USER, TC_DB_PASSWORD, TC_MINIO_API_PORT,
TC_MINIO_CONSOLE_PORT.
Huma v2 generates the OpenAPI description automatically. It is served without
authentication at /openapi.json and /openapi.yaml, with a rendered
reference at /docs. Routes live in
apps/api/internal/http/router/router.go.
- Public —
/health,/auth/*(register/login/dev-login/password-reset/oauth),/share/{token}(public calendar) - Authenticated —
/user,/calendars, and/calendars/{id}/{events,memos,members,invites,albums,labels,activity,export,import} - Admin —
/admin/oauth-providers,/admin/allowed-emails
Unauthenticated endpoints are rate-limited per client IP.
E2E tests create an independent user + calendar per test (tenant isolation) and run
in parallel against a real MySQL. There are 332 of them, 310 declaring
t.Parallel(), and the package finishes in about 64 s in CI — on ubuntu-latest
(4 vCPU) Go's default parallelism runs four at a time.
TC_DB_PORT=3307 make db-up # start MySQL on the same port CI uses
make test-e2e # runs with TC_TEST_INTEGRATION=1CI (.github/workflows/ci.yml) runs web (Biome / typecheck / Vitest) and API
(build / vet / E2E).
- The schema is layered.
sql/core/is a copy of the shared schema contract that lets more than one product write to the same database — it is not edited here and is pinned upstream insql/core/UPSTREAM.json.sql/time/holds this application's own tables. sql/build-schema.shconcatenates core then time intosql/schema.sql, whichmake db-applyapplies.make test-conformancechecks the resulting schema against the contract.make check-corechecks that the vendored copy is unedited and current.- Write queries in
sql/queries/*.sqland runmake sqlcto generate Go code intoapps/api/internal/db/generated/.
compose.prod.yml runs the stack — MySQL, object storage, the API and the built
SPA — on a single host. It builds the two images rather than pulling them: the
SPA compiles the API origin into its bundle, so no prebuilt image is correct for
two deployments.
It terminates no TLS. Both HTTP services publish on the loopback interface only
and expect a reverse proxy on the host to hold the certificates and forward to
127.0.0.1:8081 (web) and 127.0.0.1:8080 (API), under two names — the SPA
reaches the API cross-origin. Set TC_TRUSTED_PROXIES to the proxy's address, or
every client collapses onto one rate-limit bucket.
Configuration goes in a .env beside compose.prod.yml. TC_DB_ROOT_PASSWORD,
TC_DB_PASSWORD, TC_JWT_SECRET (openssl rand -hex 32), TC_WEB_URL,
TC_API_PUBLIC_URL, TC_S3_ACCESS_KEY, TC_S3_SECRET_KEY, TC_SMTP_HOST and
TC_SMTP_FROM have no safe default: Compose stops and names whichever is
missing. TC_SECRETS_KEY is optional — without it the admin panel refuses to
save OAuth client secrets rather than storing them unencrypted.
docker compose -f compose.prod.yml up -d --build
docker compose -f compose.prod.yml run --rm --entrypoint /createuser api \
-email you@example.com -password 'choose-a-long-one' -name "Your Name" -adminThe schema is applied when the MySQL volume is first created. sql/schema.sql
drops every table before creating it, so a later schema change is applied to the
running database by hand rather than by re-running the file.
Two volumes hold everything durable: mysql_data and minio_data. Back them up
together — an attachment is a row in one pointing at an object in the other.
docker compose -f compose.prod.yml exec mysql \
mysqldump -u root -p"$TC_DB_ROOT_PASSWORD" --single-transaction nodate_time > backup.sqlAGPL-3.0 — self-host, modify, and redistribute freely. Network use requires source availability.
This is an independent, original project. It is inspired by the shared-calendar experience of TimeTree, but the code, UI, and UX are its own — no source, assets, or designs are copied from any third party.
"TimeTree", "Google Calendar", and other product names are trademarks of their respective owners and are used here only for descriptive reference. This project is not affiliated with, endorsed by, or sponsored by any of them.



