Skip to content

Repository files navigation

Nodate Time

CI License: AGPL-3.0 Go React MySQL Bun

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


Screenshots

Monthly view Weekly view
Monthly view with the event-create dialog Weekly timeline view
Share & embed
Share panel with invite links and embed code

Responsive mobile layout (vertical month scroll):

Mobile monthly view

Features

  • 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

Tech stack

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)

Repo structure

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

Quick start

Prerequisites

  • Bun
  • Go 1.26+
  • Docker (for MySQL / MinIO)
  • sqlc (only when regenerating DB code)

Run

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

In development (TC_ENV=development), /auth/dev-login provides password-less login for the seeded accounts.

Common commands

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)

Environment variables

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.

API overview

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.

Testing

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=1

CI (.github/workflows/ci.yml) runs web (Biome / typecheck / Vitest) and API (build / vet / E2E).

Migrations & code generation

  • 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 in sql/core/UPSTREAM.json. sql/time/ holds this application's own tables.
  • sql/build-schema.sh concatenates core then time into sql/schema.sql, which make db-apply applies.
  • make test-conformance checks the resulting schema against the contract. make check-core checks that the vendored copy is unedited and current.
  • Write queries in sql/queries/*.sql and run make sqlc to generate Go code into apps/api/internal/db/generated/.

Deployment

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" -admin

The 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.sql

License

AGPL-3.0 — self-host, modify, and redistribute freely. Network use requires source availability.

Disclaimer & trademarks

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.

About

Self-hostable shared calendar app: member roles, invite links, recurring events, albums, audit history, and embeddable views. Go + Huma API and a React 19 SPA.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages