A project-centric organizational management and communication system: an organization has projects, projects have members and tasks, and tasks are assigned to users.
Backend: Go (Gin) · sqlc · PostgreSQL — Frontend: React + TypeScript (Vite)
Single-organization model: there is no self-serve sign-up or "create organization" flow. The one organization and its first owner account are created by a seed step (see Local Setup); from there the owner adds members 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
Organization
└── OrganizationMember (role: owner / admin / member / viewer)
Project
├── belongs to Organization
├── 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" level; RBAC is defined only at the
organizationandprojectlevels — a user can be an admin on one project and a plain member on another.
| 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/
├── cmd/
│ ├── api/ # HTTP server entrypoint (routes, wiring) — main.go
│ └── seed/ # One-time seed: creates the organization + owner account
├── internal/
│ ├── auth/ # Login, change-password, JWT issuing/parsing, password hashing
│ ├── organization/ # Organization + organization-member handlers
│ ├── project/ # Project CRUD + project-member handlers
│ ├── task/ # Task CRUD, status, assignment
│ ├── comment/ # Task comments
│ ├── rbac/ # Scope-aware role checks (organization/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)
│ ├── queries/ # Hand-written SQL used by sqlc
│ └── sqlc/ # Generated, type-safe Go from sqlc.yaml
├── web/ # React + TypeScript frontend (see Frontend Overview)
├── docker-compose.yaml # postgres + migrate + api + web
├── Dockerfile # Builds cmd/api only (cmd/seed is not containerized)
├── 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+ — needed for the manual (non-Docker) backend setup, and for the one-time seed step even when running everything else via Docker (
cmd/seedisn't built into the Docker image) - Node.js 20+ — only needed for the manual (non-Docker) frontend setup
- golang-migrate CLI — only needed for the manual backend setup
All variables live in .env (copy from .env.example). The api and seed binaries both read this file via internal/config.
| Variable | Used by | Description |
|---|---|---|
APP_ENV |
api | development, production, or test — controls Gin's mode |
APP_PORT |
api | Port the API listens on (default 8080) |
DATABASE_URL |
api, seed | Full Postgres connection string; takes priority when set |
DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME / DB_SSLMODE |
api, docker-compose | Used to build the connection string / to configure the postgres container |
JWT_SECRET |
api | Required — the API refuses to start if this is empty |
JWT_ACCESS_TOKEN_TTL |
api | Access token lifetime (e.g. 15m) |
JWT_REFRESH_TOKEN_TTL |
api | Refresh token lifetime (e.g. 720h) — issued today, but there's no /auth/refresh route yet to redeem it |
ORG_NAME |
seed | Display name of the single organization created on first run |
ORG_SLUG |
seed, web build | Organization slug; also passed as VITE_ORG_SLUG to the frontend build |
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 |
seedfails fast ifOWNER_EMAIL,OWNER_NAME, orOWNER_PASSWORDare empty. It's a no-op (prints a message and exits 0) if an organization 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.
# ORG_NAME / ORG_SLUG / OWNER_EMAIL / OWNER_NAME / OWNER_PASSWORD are used by the seed step below.
docker compose up --buildThis starts everything: postgres → migrations run automatically via the migrate service → api on http://localhost:8080 → web on http://localhost:3000.
web'sVITE_API_URLbuild arg is empty by default indocker-compose.yaml, so the frontend falls back tohttp://localhost:8080for the API at build time. If you're deployingapiandwebon different hosts, setVITE_API_URLaccordingly before building.
Seed the first organization and owner account (one-time, required before you can log in — there's no docker-compose service for this yet, so it's run locally against the Dockerized Postgres):
export DATABASE_URL="postgres://mitra:mitra@localhost:5432/mitra?sslmode=disable"
go run ./cmd/seedThis reads ORG_NAME, ORG_SLUG, OWNER_EMAIL, OWNER_NAME, and OWNER_PASSWORD from .env and creates the organization plus its owner.
# 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"
migrate -database "$DATABASE_URL" -path internal/db/migrations up
# 4. Seed the first organization and owner account (one-time, required before you can log in)
go run ./cmd/seed
# 5. Run the server
go run ./cmd/api
# health check: curl http://localhost:8080/healthcd web
npm install
npm run devBy default the frontend talks to http://localhost:8080. Set VITE_API_URL in web/.env to point elsewhere, and VITE_ORG_SLUG to match ORG_SLUG from the backend .env.
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 org/project admin adding a member — see Organizations. 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 | /organizations/by-slug/:slug |
Get organization by slug |
| GET | /organizations/:id/members |
List members |
| POST | /organizations/:id/members |
Add member |
| DELETE | /organizations/:id/members/:user_id |
Remove member |
| POST | /organizations/:id/projects |
Create project in organization |
| GET | /organizations/:id/projects |
List organization's projects |
No
POST /organizations— self-serve organization creation was removed; the single organization is created by the seed step instead.
| Method | Path | Description |
|---|---|---|
| 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/users/me |
api/users.ts (profile page) |
| PATCH | /v1/users/me |
api/users.ts (profile page) |
| 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.
- Routing (
src/router.tsx): auth pages (login, forced password change), dashboard, project list/detail with a task board, task detail, organization members/settings, profile, chat, and notifications.components/guards/RouteGuards.tsxgates routes on auth state;components/organizations/OrgGate.tsxgates on organization membership. - State (
src/stores/): one Zustand store per domain —auth,organization,project,task,notification,toast,ui. - API layer (
src/api/): a thin axios client (client.ts) plus one module per resource (auth,organizations,projects,tasks,comments,notifications,users). Thenotificationsandusersmodules call 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 org/project owner-or-admin checks 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 | Organization scope | Project scope |
|---|---|---|
owner |
Full control; set once by the seed step | Full control over that project |
admin |
Manage members/projects, same as owner for most checks | Manage members/tasks, same as project owner for most checks |
member |
Default role for anyone added to the organization | 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: IsOrganizationMember, IsOrganizationOwnerOrAdmin, 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 or organization creation — by design for now; see the note under Organizations.
- Seeding isn't containerized —
cmd/seedhas to be run withgo run(locally or in CI), even in the Docker setup; there's nodocker-composeservice for it yet. - 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, organization/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.