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.
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.
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:
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
- Copy
.env.exampleto.envfor shared defaults, and copy.env.development.example/.env.production.exampleto.env.development/.env.productionif those files do not exist. These local files are Git-ignored. - Create an application in the Discord Developer Portal.
Under OAuth2, copy the client ID and client secret into
DISCORD_CLIENT_IDandDISCORD_CLIENT_SECRETin.env. - Set
AUTH_COOKIE_SECRETto a random secret of at least 32 characters, generated withopenssl rand -hex 32. Keep it server-side along with the client secret. - Set
APP_ORIGINin.env.developmentto the development browser origin, initiallyhttp://127.0.0.1:3000, and set the same variable in.env.productionto the production origin, such ashttps://habits.example.com. Register both origins with/api/auth/discord/callbackappended as redirect URIs in Discord. Use the matching hostname throughout each sign-in;localhostand127.0.0.1have different cookie storage. - Run
bun dev, click Sign in with Discord, and authorize theidentifyscope. 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.
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:
{
"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:
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.
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.