A project-centric work management and communication system: the deployment is single-tenant (no organizations/tenants), projects have members and tasks, and tasks are assigned to users.
Backend: Go (Gin) · sqlc · PostgreSQL — Frontend: React + TypeScript (Vite)
Single-tenant model: there is no self-serve sign-up or multi-tenant concept — the app was built for one deployment only. The first owner account is created by a seed step (see Local Setup); from there the owner adds users through the API/UI. There is currently no
/auth/registerendpoint.
This is a Phase 1 / MVP snapshot — see Known Limitations for what's intentionally not built yet, and MITRA.md for the full architecture proposal and roadmap (in Persian).
- Mitra
User (global role: owner / admin / member / viewer)
Project
├── belongs directly to the deployment (single-tenant, no Organization level)
├── ProjectMember (project-level role: owner / admin / member / viewer)
└── Task
├── assigned to a single User (not a Team — this system has no Team concept)
├── status: todo / in_progress / review / done
├── priority: low / medium / high / urgent
└── Comment
This project is intentionally designed without a "Team" or "Organization" level; RBAC is defined only at the
user(global) andprojectlevels — a user can be an admin on one project and a plain member on another, on top of their global role.
| Layer | Technology |
|---|---|
| Backend | Go 1.27 + Gin |
| Data Access | sqlc (no ORM) on top of pgx/v5 |
| Migrations | golang-migrate |
| Auth | JWT (access + refresh) via golang-jwt/v5, passwords hashed with bcrypt |
| Database | PostgreSQL 16 |
| Frontend | React 19 + TypeScript + Vite 8, Tailwind CSS 4 |
| Frontend state | Zustand (per-domain stores), React Router 7 |
| i18n | Custom context-based i18n, Persian (fa) and English (en), RTL-aware UI |
Current architectural decisions (cost control in Phase 1):
Redis and NATS/JetStream have been removed from the stack for now. Details and the criteria for bringing them back are documented in MITRA.md.
mitra/
├── main.go # Single entrypoint — delegates to cmd.Execute()
├── cmd/ # Cobra CLI: `mitra serve` (API server), `mitra migrate` (up/down/steps/force/version), `mitra seed`
├── internal/
│ ├── auth/ # Login, change-password, JWT issuing/parsing, password hashing
│ ├── users/ # User directory (list/create/delete), profile (me/update-me)
│ ├── project/ # Project CRUD + project-member handlers
│ ├── task/ # Task CRUD, status, assignment
│ ├── comment/ # Task comments
│ ├── rbac/ # Scope-aware role checks (global user role / project owner|admin)
│ ├── middleware/ # Auth middleware (Bearer token → user context)
│ ├── config/ # Env loading (caarlos0/env + godotenv)
│ ├── convert/ # Shared helpers (e.g. flexible date parsing)
│ └── db/
│ ├── migrations/ # golang-migrate SQL migrations (001–007), embedded into the binary
│ ├── migrator/ # golang-migrate wrapper used by both `mitra serve` (auto-migrate) and `mitra migrate`
│ ├── queries/ # Hand-written SQL used by sqlc
│ └── sqlc/ # Generated, type-safe Go from sqlc.yaml
├── web/ # React + TypeScript frontend (see Frontend Overview) + embed.go, which embeds web/dist into the mitra binary
├── docker-compose.yaml # postgres + api (builds the frontend and embeds it; no separate web service/port)
├── Dockerfile # Multi-stage: builds web/dist, then embeds it into the single `mitra` binary (serve/migrate/seed all included)
├── sqlc.yaml
├── MITRA.md # Full architecture proposal & phased roadmap (Persian)
├── README.md / README.fa.md
└── .env.example
- Docker + Docker Compose — for the all-in-one setup, or just to run PostgreSQL locally
- Go 1.27+ — only needed for the manual (non-Docker) backend setup; migrations and seeding are both subcommands of the same
mitrabinary now, so there's nothing extra to install for them - Node.js 20+ — needed for frontend dev (
npm run dev), and at least once for any local (non-Docker)go run . serve/go build, since it embedsweb/dist(see Frontend is embedded)
All variables live in .env (copy from .env.example). Every mitra subcommand (serve, migrate, seed) reads this file via internal/config.
| Variable | Used by | Description |
|---|---|---|
APP_ENV |
serve | development, production, or test — controls Gin's mode |
APP_PORT |
serve | Port the API listens on (default 8080) |
DATABASE_URL |
serve, migrate, seed | Full Postgres connection string; takes priority when set |
DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME / DB_SSLMODE |
serve, migrate, seed, docker-compose | Used to build the connection string / to configure the postgres container |
AUTO_MIGRATE |
serve | Default true — mitra serve applies pending migrations itself on startup before accepting requests. Set to false to manage migrations only via mitra migrate |
JWT_SECRET |
serve | Required — the API refuses to start if this is empty |
JWT_ACCESS_TOKEN_TTL |
serve | Access token lifetime (e.g. 15m) |
JWT_REFRESH_TOKEN_TTL |
serve | Refresh token lifetime (e.g. 720h) — issued today, but there's no /auth/refresh route yet to redeem it |
OWNER_EMAIL |
seed | Login email for the seeded owner account |
OWNER_NAME |
seed | Full name for the seeded owner account |
OWNER_PASSWORD |
seed | Initial password for the seeded owner account — change it after first login |
mitra seedfails fast ifOWNER_EMAIL,OWNER_NAME, orOWNER_PASSWORDare empty. It's a no-op (prints a message and exits 0) if a user already exists, so it's safe to re-run.
cp .env.example .env
# JWT_SECRET is required — the api will fail to start if it's empty.
# The placeholder value in .env.example works for localhost only;
# replace it with a real random secret for anything beyond that.
# OWNER_EMAIL / OWNER_NAME / OWNER_PASSWORD are used by the seed step below.
docker compose up --buildThis starts everything: postgres → api runs pending migrations itself on startup (see AUTO_MIGRATE), builds the frontend, and serves both the API and the UI from http://localhost:8080.
The frontend is embedded into the
mitrabinary at build time (seeweb/embed.go) and served by the same process that serves the API — there's no separate frontend container or port anymore.web/Dockerfileandweb/nginx.confstill exist for the rare case you'd want to host the frontend standalone (e.g. behind a CDN), but the default Docker flow above doesn't use them.
Seed the owner account (one-time, required before you can log in). Since seed is just a subcommand of the same binary as api, run it against the running container — no separate go install needed:
docker compose run --rm api ./mitra seedThis reads OWNER_EMAIL, OWNER_NAME, and OWNER_PASSWORD from .env and creates the owner account. Every value can also be passed as a flag instead (--owner-email, --owner-name, --owner-password), which takes priority over the env var when given — handy for scripting/CI without touching .env. Prefer the env var for the password where you can, since flag values are visible in shell history and ps.
# 1. Start the database only
docker compose up -d postgres
# 2. Configure env
cp .env.example .env
# Make sure to replace JWT_SECRET with a secure, random value
# 3. Run migrations
export DATABASE_URL="postgres://mitra:mitra@localhost:5432/mitra?sslmode=disable"
go run . migrate up
# 4. Seed the owner account (one-time, required before you can log in)
go run . seed
# 5. Build the frontend — required at least once, since `go run . serve`
# embeds whatever is currently in web/dist (see "Frontend is embedded" below)
cd web && npm install && npm run build && cd ..
# 6. Run the server
go run . serve
# UI: http://localhost:8080 · health check: curl http://localhost:8080/health
go run . migrate upis optional here — by defaultmitra serve(step 6) applies pending migrations itself on startup. Run it explicitly if you'd rather control migrations separately (setAUTO_MIGRATE=falsein that case). Other migration subcommands:go run . migrate down,migrate steps <n>,migrate force <version>,migrate version.
web/embed.go embeds web/dist into the mitra binary via go:embed, and mitra serve serves it directly — that's how docker compose up gives you both API and UI on one port. This has one consequence for local (non-Docker) development: web/dist has to contain a real build before go build/go run in this module serves real UI files. web/dist is git-ignored (only a .gitkeep placeholder is tracked), so on a fresh clone the package still compiles fine, it just has nothing real to serve until you run step 5 above. Re-run npm run build any time you change the frontend and want go run . serve to reflect it — there's no live-reload here, that's what npm run dev below is for.
For active frontend development, run Vite's dev server instead of rebuilding on every change:
cd web
npm install
npm run devThis proxies /api to http://localhost:8080 (see vite.config.ts) and gives you hot reload — it doesn't touch web/dist or the embedded build.
Base path: /api/v1 (except /health, which is unversioned)
| Method | Path | Description |
|---|---|---|
| GET | /health |
Liveness/health check |
| Method | Path | Description |
|---|---|---|
| POST | /auth/login |
Login |
| POST | /auth/change-password |
Change own password (requires Authorization: Bearer) |
There is no
/auth/register. Accounts are created either by the seed step (the first owner) or by an owner/admin adding a user — see Users. Login responses includemust_change_password; the frontend routes users with that flag set to a forced password-change screen before letting them in.
| Method | Path | Description |
|---|---|---|
| GET | /users/me |
Get your own profile |
| PATCH | /users/me |
Update your own profile (full_name) |
| GET | /users |
List all users |
| POST | /users |
Create a user (owner/admin only; only an owner can create another owner) — returns a temp_password |
| DELETE | /users/:id |
Remove (soft-delete) a user (owner/admin only; can't remove yourself; only an owner can remove another owner) |
There's no separate "organization" resource — the deployment is single-tenant, so this list is simply every user account. Global role (
owner/admin/member/viewer) lives directly onusers.role.
| Method | Path | Description |
|---|---|---|
| POST | /projects |
Create project (owner/admin only) |
| GET | /projects |
List all projects |
| GET | /projects/:id |
Project details |
| PUT | /projects/:id |
Edit project |
| DELETE | /projects/:id |
Delete (soft) project |
| GET | /projects/:id/members |
List project members |
| POST | /projects/:id/members |
Add member |
| DELETE | /projects/:id/members/:user_id |
Remove member |
| POST | /projects/:id/tasks |
Create task in project |
| GET | /projects/:id/tasks |
List project's tasks |
| Method | Path | Description |
|---|---|---|
| GET | /tasks/assigned-to-me |
Tasks assigned to me |
| GET | /tasks/:id |
Task details |
| PUT | /tasks/:id |
Edit task |
| PATCH | /tasks/:id/status |
Change status |
| POST | /tasks/:id/assign/user |
Assign to a user |
| POST | /tasks/:id/unassign |
Unassign |
| DELETE | /tasks/:id |
Delete (soft) |
| GET | /tasks/:id/comments |
List task comments |
| POST | /tasks/:id/comments |
Add comment |
| Method | Path | Description |
|---|---|---|
| PUT | /comments/:id |
Edit comment |
| DELETE | /comments/:id |
Delete comment |
The web client already has API/store/hook code for these — they currently 404 against this backend:
| Method | Path | Used by (frontend) |
|---|---|---|
| GET | /v1/notifications |
api/notifications.ts, notifications store |
| PATCH | /v1/notifications/:id/read |
api/notifications.ts |
| PATCH | /v1/notifications/read-all |
api/notifications.ts |
| WS | (a websocket endpoint) | hooks/use-websocket.ts, chat page |
None of these have a corresponding Go handler yet — see Known Limitations.
React 19 + TypeScript app in web/, built with Vite and styled with Tailwind CSS 4. In production it's embedded into the mitra Go binary and served by the same process as the API (see Frontend is embedded) — there's no separate frontend server/container to run.
- Routing (
src/router.tsx): auth pages (login, forced password change), dashboard, project list/detail with a task board, task detail, team (user directory/management, at/team), profile, chat, and notifications.components/guards/RouteGuards.tsxgates routes on auth state. - State (
src/stores/): one Zustand store per domain —auth,users,project,task,notification,toast,ui. - API layer (
src/api/): a thin axios client (client.ts) plus one module per resource (auth,projects,tasks,comments,notifications,users). Thenotificationsmodule calls endpoints the backend doesn't expose yet (see the table above). - Realtime:
hooks/use-websocket.tsis a generic reconnecting-WebSocket hook, used by the chat page — there's no WebSocket server on the backend yet (Phase 2, seeMITRA.md). - i18n:
src/i18n/provides Persian (fa.ts) and English (en.ts) dictionaries behind a React context, with RTL-aware components (DirectionalIcon,LanguageSwitcher) and a Vazirmatn variable font for Persian. - UI kit: a small local component library in
src/components/ui/(Button, Card, Modal, Toaster, DonutChart, StatCard, etc.) rather than a third-party design system. - Permissions:
src/lib/permissions.tsmirrors the backend's global-role/project owner-or-admin checks (canManageUsers,canRemoveUser,canManageProject) so the UI can hide actions the API would reject.
Roles are free-form VARCHAR values (no DB-level enum), but the app treats these as the valid set at both scopes:
| Role | Global scope (users.role) |
Project scope |
|---|---|---|
owner |
Full control; set once by the seed step | Full control over that project |
admin |
Manage users/projects, same as owner for most checks | Manage members/tasks, same as project owner for most checks |
member |
Default role for anyone added by an owner/admin | Default role for anyone added to a project |
viewer |
Read-only (per the hierarchy diagram) | Read-only (per the hierarchy diagram) |
internal/rbac/policy.go implements the checks actually enforced today: GetUserRole/IsOwnerOrAdmin (global role, read straight off users.role — no separate membership table anymore), IsProjectMember, IsProjectOwnerOrAdmin — i.e. most write actions currently just require "member" or "owner/admin", not a fully granular per-permission model yet (that's Phase 3 in MITRA.md).
- No
/auth/refreshendpoint yet — the frontend's axios client already has retry logic wired up to call it on a 401, but the backend doesn't implement this route yet, so an expired access token currently just logs the user out and requires a fresh login. - No self-serve registration — by design for now, single-tenant deployment; see the note under Users.
- Seeding requires a manual step —
mitra seedhas to be run once explicitly (viadocker compose run --rm api ./mitra seedorgo run . seed); it's not triggered automatically since it depends on theOWNER_*env vars you set per-deployment. - Frontend/backend gap — the web app already has UI, stores, and API calls for a user profile endpoint, notifications, and a WebSocket connection (chat), none of which exist on the backend yet. See the table in API.
- Presence/Realtime/Push notifications are not yet implemented (Phase 2).
- No Redis/NATS — removed for cost control in Phase 1; rationale and temporary in-process workaround documented in
MITRA.md. - No automated tests in this snapshot (
internal/,web/) — sqlc queries and handlers are not yet covered by integration tests.
Summarized from MITRA.md (full detail and rationale there, in Persian):
- Phase 1 — Core MVP (current): auth, user/project/task CRUD, task comments, basic dashboard, scope-aware RBAC. ✅ mostly done, gaps listed above.
- Phase 2 — Communication & Realtime: in-app chat over WebSocket (in-process hub, no NATS yet), push notifications (direct FCM calls, no queue yet), live task-status updates.
- Phase 3 — Advanced access & reporting: full RBAC with project-level overrides, activity-log-based reporting, advanced filtering/search.
- Phase 4 — Desktop & optimization: Tauri desktop packaging around the same React codebase, full offline mode for the (planned) Flutter mobile app, revisit bringing Redis/NATS back if horizontal scaling is actually needed.
MIT — see LICENSE.