# Minabot Minimal Bun + Hono backend with Drizzle ORM and local SQLite, and a React Router client initialized with Bun's shadcn + Tailwind CSS v4 template. ```sh bun install bun dev ``` Open http://127.0.0.1:3000. Set `PORT` to use a different port. - Client pages: `/`, `/about`, `/settings`. - Hono endpoint: `GET /api/health`. - Unknown API routes return JSON with a 404 status. Pages contain a heading, native navigation buttons, and account controls. Tailwind utilities and shadcn configuration are available; Tailwind Preflight is omitted to preserve native browser styling. ```sh bun run typecheck bun run build bun start ``` The build includes the backend and frontend in `dist/`. `bun start` serves that production build. Client routes support direct loading and browser history. ## Database The backend uses Drizzle ORM with Bun's built-in SQLite driver. The database is created at `data/minabot.sqlite`, with WAL mode and foreign keys enabled. Database files are ignored by Git. Development and production use the same file. Optionally copy `.env.example` to `.env` and set `DATABASE_PATH` to another local file. Environment-specific database paths can be set in `.env.development` and `.env.production`. Relative paths are resolved from the project root by the package scripts. Define tables in `src/db/schema.ts`, then generate and apply migrations: ```sh bun run db:generate --name describe_change bun run db:migrate ``` Commit the generated `drizzle/` files alongside schema changes. `bun dev` and `bun start` apply pending migrations before starting the server. The initial migration creates the users and sessions tables. Import `db` from `src/db/index.ts` in backend code to query the database. `GET /api/health` checks the database connection before returning success. ## Discord sign-in 1. Copy `.env.example` to `.env` for shared defaults, and copy `.env.development.example` / `.env.production.example` to `.env.development` / `.env.production` if those files do not exist. These local files are Git-ignored. 2. Create an application in the [Discord Developer Portal](https://discord.com/developers/applications). Under OAuth2, copy the client ID and client secret into `DISCORD_CLIENT_ID` and `DISCORD_CLIENT_SECRET` in `.env`. 3. Set `AUTH_COOKIE_SECRET` to a random secret of at least 32 characters, generated with `openssl rand -hex 32`. Keep it server-side along with the client secret. 4. Set `APP_ORIGIN` in `.env.development` to the development browser origin, initially `http://127.0.0.1:3000`, and set the same variable in `.env.production` to the production origin, such as `https://habits.example.com`. Register **both** origins with `/api/auth/discord/callback` appended as redirect URIs in Discord. Use the matching hostname throughout each sign-in; `localhost` and `127.0.0.1` have different cookie storage. 5. Run `bun dev`, click **Sign in with Discord**, and authorize the `identify` scope. No bot token or email scope is needed. After returning, **View my profile** opens `/api/me`. **Sign out** revokes the current browser's session. Without the OAuth settings, public pages and `/api/health` remain usable, `/api/me` returns 401, and sign-in displays a configuration message. Restart after editing any env file. Use HTTPS for non-local deployments; HTTP is only allowed on loopback origins. When using a reverse proxy, preserve the public request host and serve both React and `/api` from the selected origin. The package scripts explicitly select the environment before migrations or server startup: | Command | Environment file | Mode | | --- | --- | --- | | `bun dev` | `.env.development` | development | | `bun start` | `.env.production` | production | | `bun run db:generate` / `bun run db:migrate` | `.env.development` | development | | `bun run db:migrate:production` | `.env.production` | production | Each command loads optional `.env` shared defaults, then the selected file. Variables exported by the shell or deployment platform take precedence over files. The selected `APP_ORIGIN` controls OAuth callbacks, cookie security, and logout Origin validation. Production requires an explicit `APP_ORIGIN`; development can fall back to `http://127.0.0.1:$PORT` (port 3000 if unset). To test `bun start` locally, set `APP_ORIGIN=http://127.0.0.1:3000` in `.env.production`. Automatic env-file loading is disabled in `bunfig.toml` so the outer `bun start` process cannot preload development settings into production. Package scripts load the files explicitly using Bun's `--env-file` option; `.env.local` files are not loaded by these commands. Build, test, and typecheck do not load application env files. For custom Bun commands, pass the desired `--env-file` flags explicitly. See [Bun environment variables](https://bun.sh/docs/runtime/environment-variables). Use `APP_ORIGIN` in each file; `APP_ORIGIN_DEV` and `APP_ORIGIN_PROD` are no longer used. Credentials can stay in `.env` or be overridden separately in each file. For deployments sharing a machine, set distinct `DATABASE_PATH` values to keep production habit data separate from development data. ### Schema and session behavior - `users`: app UUID, unique Discord ID stored as text, username, nullable display name and avatar hash, IANA timezone, and creation/update timestamps. - `sessions`: SHA-256 token hash, user foreign key with cascading deletion, creation timestamp, and expiry timestamp. User and expiry indexes support session management and cleanup. - Timestamps are Unix milliseconds. Each session expires 30 days after sign-in; activity does not extend it. A new sign-in replaces the current browser's session. Other browsers retain independent sessions. Expired sessions are removed during sign-in or when presented to an authenticated route. - Cookies are HttpOnly, SameSite=Lax, and host-only. HTTPS enables Secure and `__Host-` cookie names. OAuth state uses a signed cookie with a ten-minute lifetime and is cleared on callback. Token exchange and profile retrieval happen server-side; Discord access/refresh tokens are never persisted or sent to React. - React detects the browser timezone during initial registration. The server validates it using `Intl.DateTimeFormat`, falling back to UTC. Later sign-ins update the Discord profile without changing the saved timezone. A timezone settings editor and habit reset logic are future work. Habit timestamps should remain UTC; daily boundaries should be calculated using the user's timezone. ### API | Method | Route | Behavior | | --- | --- | --- | | GET | `/api/health` | Public database health check | | GET | `/api/auth/discord?timezone=Europe%2FBelgrade` | Start sign-in | | GET | `/api/auth/discord/callback` | Validate OAuth response and create a session | | POST | `/api/auth/logout` | Revoke current session; requires matching Origin | | GET | `/api/me` | Public user fields for the current session, or 401 | `/api/me` returns the object directly: ```json { "id": "app-generated-uuid", "discordId": "123456789012345678", "username": "mina", "displayName": "Mina", "avatarUrl": null, "timezone": "Europe/Belgrade" } ``` Only the fields declared in `src/shared/user.ts` are returned. Missing/invalid or expired sessions receive `401 {"error":"Unauthorized"}`. Authentication endpoints and `/api/me` use `Cache-Control: no-store`. In `src/api.ts`, protect another route with the same middleware: ```ts app.get("/api/example", auth.requireAuth, c => c.json({ userId: c.get("user").id })); app.post("/api/example", auth.requireSameOrigin, auth.requireAuth, c => { // Validate input and enforce ownership using c.get("user").id. return c.json({ ok: true }); }); ``` Use `requireSameOrigin` for all cookie-authenticated mutations. Same-origin browser fetches send the cookie automatically; tokens should not be placed in localStorage. ```sh bun test bun run typecheck bun run build ``` Tests use an isolated in-memory SQLite database with the real migrations and a stubbed Discord HTTP client. They cover registration, profile refresh, timezone preservation, cookie security, invalid/expired state, provider failures, session expiry/rotation, logout CSRF checks, and the exact `/api/me` response. A real Discord consent flow additionally requires your application credentials and browser login.