Team OS — Micro HRIS for Advisory OS
App Specification
Overview
Team OS is a lightweight people management tool for professional service practice owners. It tracks team interactions, goals, and generates evidence-based review summaries — eliminating the recency bias and memory-based reviews that plague small firms.
Domain: team-os.advisoryos.ai (frontend) / team-os-api.advisoryos.ai (backend)
Tech Stack
| Layer | Technology | Host |
|---|---|---|
| Frontend | React + Vite | Vercel |
| Backend | Express.js + Node.js | Railway |
| Database | PostgreSQL | Railway |
| Auth | JWT (access + refresh tokens) | — |
| Resend (password reset) | — |
Design System (Advisory OS)
| Element | Value |
|---|---|
| Primary | Charcoal #1a1a1a |
| Background | Cream #f9f7f4 |
| Accent | Gold #b79d64 |
| Heading Font | Cormorant Garamond |
| Body Font | Inter |
| Border Radius | 0px (sharp edges everywhere) |
| Card Style | Gold left-border (3px), cream background |
| Scrollbar | Gold track on charcoal |
| Layout | Desktop only (no mobile optimization) |
Category Badge Colors
| Category | Background | Text |
|---|---|---|
| Win | rgba(143,209,158,0.2) | #2d6a4f |
| Concern | rgba(239,68,68,0.15) | #991b1b |
| Feedback | rgba(59,130,246,0.2) | #1e40af |
| Commitment | rgba(168,85,247,0.2) | #581c87 |
| 1:1 Meeting | rgba(183,157,100,0.2) | #7c6a3a |
| Absence | rgba(156,163,175,0.2) | #4b5563 |
Goal Status Colors
| Status | Background | Text |
|---|---|---|
| On Track | rgba(143,209,158,0.2) | #2d6a4f |
| At Risk | rgba(251,191,36,0.2) | #92400e |
| Completed | rgba(183,157,100,0.2) | #7c6a3a |
| Paused | rgba(156,163,175,0.2) | #4b5563 |
Database Schema
users
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(255),
reset_token_hash VARCHAR(255),
reset_token_expires TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
team_members
CREATE TABLE team_members (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
role VARCHAR(255),
status VARCHAR(20) DEFAULT 'active', -- active, on_leave, former
start_date DATE,
email VARCHAR(255),
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
interactions
CREATE TABLE interactions (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
team_member_id INTEGER REFERENCES team_members(id) ON DELETE CASCADE,
category VARCHAR(20) NOT NULL, -- win, concern, feedback, commitment, meeting, absence
content TEXT NOT NULL,
interaction_date TIMESTAMPTZ DEFAULT NOW(),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
goals
CREATE TABLE goals (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
team_member_id INTEGER REFERENCES team_members(id) ON DELETE CASCADE,
title TEXT NOT NULL,
status VARCHAR(20) DEFAULT 'on_track', -- on_track, at_risk, completed, paused
target_date DATE,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
review_snapshots
CREATE TABLE review_snapshots (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
team_member_id INTEGER REFERENCES team_members(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL, -- e.g. "Q1 2026 Review"
period_start DATE,
period_end DATE,
summary_text TEXT, -- saved narrative
interaction_count INTEGER,
win_count INTEGER,
concern_count INTEGER,
feedback_count INTEGER,
commitment_count INTEGER,
meeting_count INTEGER,
absence_count INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW()
);
API Routes
Auth
POST /api/auth/register— Create accountPOST /api/auth/login— Login, returns JWT tokensPOST /api/auth/refresh— Refresh access tokenPOST /api/auth/forgot-password— Send reset emailPOST /api/auth/reset-password— Reset with token
Team Members
GET /api/team-members— List all (with interaction counts)POST /api/team-members— CreateGET /api/team-members/:id— Get one (with full counts)PUT /api/team-members/:id— UpdateDELETE /api/team-members/:id— Soft delete (set status to 'former')
Interactions
GET /api/team-members/:id/interactions— List for person (with filters)POST /api/team-members/:id/interactions— Add interactionPUT /api/interactions/:id— EditDELETE /api/interactions/:id— Delete
Query params for filtering:
?category=win,feedback— Filter by category (comma-separated)?from=2026-01-01&to=2026-03-31— Date range?sort=desc— Sort by date (default desc)
Goals
GET /api/team-members/:id/goals— List for personPOST /api/team-members/:id/goals— CreatePUT /api/goals/:id— Update (status, notes, title)DELETE /api/goals/:id— Delete
Reviews
GET /api/team-members/:id/review— Generate review data on-the-fly (counts + interactions grouped by category for a date range)POST /api/team-members/:id/review-snapshots— Save a review snapshotGET /api/team-members/:id/review-snapshots— List saved snapshotsGET /api/review-snapshots/:id— Get saved snapshot
Health
GET /health— Health check
Frontend Views
1. Team Roster (Default View — /)
The landing page. Grid of person cards showing everyone at a glance.
Card contents:
- Name (Cormorant Garamond, 18px)
- Role (Inter, 13px, muted)
- Status badge (Active = green, On Leave = amber, Former = gray)
- Three stat boxes in a row: Wins count | Concerns count | Total interactions
- Gold left-border on card (3px)
Actions:
- "Add Team Member" button (top right, gold background, charcoal text)
- Click any card → navigates to Person Detail view
- Status filter dropdown (All, Active, On Leave, Former)
Add Team Member modal:
- Name (required)
- Role
- Start Date
- Notes
- Status dropdown (Active, On Leave, Former)
2. Person Detail View (/team/:id)
Three-tab layout for a single person. Tabs: Interactions | Goals | Review
Person header (above tabs):
- Name + Role
- Status badge
- Start date
- "Edit" button (opens edit modal)
- "Back to Roster" link
Tab: Interactions (default)
Reverse-chronological list of all interactions for this person.
Each entry shows:
- Date + time (left column, small text)
- Category badge (colored pill: Win, Concern, Feedback, Commitment, 1:1 Meeting, Absence)
- Content text
- Edit/Delete icons (on hover)
Actions:
- "Log Interaction" button (gold, top right)
- Category filter pills (toggle on/off, show counts)
- Date range filter (From / To date pickers)
Log Interaction modal:
- Category dropdown (Win, Concern, Feedback, Commitment, 1:1 Meeting, Absence)
- Date + time (defaults to now)
- Content (textarea, required)
- Save / Cancel
Tab: Goals
List of active goals for this person with status tracking.
Each goal row shows:
- Goal title text
- Target date (if set)
- Status badge (On Track = green, At Risk = amber, Completed = gold, Paused = gray)
- Click to expand → shows notes, edit status, edit title, edit target date
Actions:
- "Add Goal" button (gold, top right)
- Filter by status (All, On Track, At Risk, Completed, Paused)
Add Goal modal:
- Title (required)
- Status dropdown
- Target date (optional)
- Notes (optional)
Tab: Review
Auto-generated review summary from interaction data. NOT AI-generated — this is simple data aggregation.
Summary counts grid (2 rows of 3):
| Wins | Concerns | Feedback |
| Commitments | 1:1 Meetings | Absences |
Each count box: large number (Cormorant Garamond), label below (Inter, small caps)
Date range selector:
- Preset buttons: Last 30 Days | Last 90 Days | This Year | All Time
- Custom date range (From / To)
Grouped interaction sections (below counts):
- "Key Wins" — lists all win interactions in the date range
- "Concerns" — lists all concern interactions
- "Commitments" — lists all commitments with dates
- "Feedback Given" — lists all feedback entries
- "1:1 Meeting Notes" — lists all meeting entries
Each section only shows if there are entries. Each entry shows date + content.
Actions:
- "Save Snapshot" button — saves current counts + date range as a review_snapshot
- "View Saved Reviews" — shows list of previous snapshots with dates
3. Auth Pages
- Login (
/login) - Register (
/register) - Forgot Password (
/forgot-password) - Reset Password (
/reset-password/:token)
Same patterns as Rapport: login is default if not authenticated, gold accent button, charcoal background for auth pages.
Frontend Architecture
client/
├── src/
│ ├── api/
│ │ └── index.js # API client with auth headers
│ ├── components/
│ │ ├── Layout.jsx # App shell with nav
│ │ ├── TeamCard.jsx # Person card for roster grid
│ │ ├── InteractionEntry.jsx
│ │ ├── GoalRow.jsx
│ │ ├── ReviewSummary.jsx
│ │ ├── CategoryBadge.jsx
│ │ ├── StatusBadge.jsx
│ │ ├── Modal.jsx # Reusable modal
│ │ └── DateRangeFilter.jsx
│ ├── pages/
│ │ ├── Roster.jsx # Team roster grid
│ │ ├── PersonDetail.jsx # Tabbed person view
│ │ ├── Login.jsx
│ │ ├── Register.jsx
│ │ ├── ForgotPassword.jsx
│ │ └── ResetPassword.jsx
│ ├── App.jsx
│ └── main.jsx
├── index.html
├── vite.config.js
└── vercel.json # SPA rewrites
Backend Architecture
server/
├── index.js # Express app setup, CORS, routes
├── db.js # PostgreSQL pool
├── middleware/
│ └── auth.js # JWT verification
├── routes/
│ ├── auth.js
│ ├── teamMembers.js
│ ├── interactions.js
│ ├── goals.js
│ └── reviews.js
└── package.json
Build Order
- Backend: Database + API
- Set up Express server with PostgreSQL connection
- Create tables
- Build all API routes
- Add auth middleware
- Frontend: Team Roster View
- Team roster grid with person cards
- Add/Edit team member modals
- Status filtering
- Frontend: Person Detail — Interactions Tab
- Interaction list with category badges
- Log interaction modal
- Category + date filtering
- Frontend: Person Detail — Goals Tab
- Goal list with status badges
- Add/edit goals
- Status filtering
- Frontend: Person Detail — Review Tab
- Count summary grid
- Date range presets + custom range
- Grouped interaction sections
- Save/view snapshots
- Frontend: Auth Pages
- Login, Register, Forgot/Reset Password
- JWT token management
Server Configuration
// Bind to 0.0.0.0 for Railway
app.listen(PORT, '0.0.0.0', () => {...})
// CORS - allow frontend domain
cors({
origin: [
'http://localhost:5173',
'https://team-os.advisoryos.ai'
],
credentials: true
})
// API URL detection (client-side)
const API_BASE = window.location.hostname === 'localhost'
? 'http://localhost:3001/api'
: 'https://team-os-api.advisoryos.ai/api'
Deployment Checklist
Railway (Backend)
- [ ] New project with PostgreSQL
- [ ] Add GitHub repo, root directory:
server - [ ] Environment variables: DATABASEURL, NODEENV, JWTSECRET, JWTREFRESHSECRET, RESENDAPIKEY, RESENDFROMEMAIL, CLIENTURL
- [ ] Custom domain: team-os-api.advisoryos.ai (CNAME to Railway URL)
Vercel (Frontend)
- [ ] Add project from GitHub repo, root directory:
client - [ ] Framework preset: Vite
- [ ] Environment variable: VITEAPIURL
- [ ] Custom domain: team-os.advisoryos.ai (CNAME to cname.vercel-dns.com)
- [ ] Add vercel.json with SPA rewrites
DNS (advisoryos.ai)
- [ ] CNAME:
team-os→ cname.vercel-dns.com - [ ] CNAME:
team-os-api→ [railway-url].up.railway.app
Key Behaviors
- Interaction logging is fast — Modal opens, category dropdown + textarea, save. Under 10 seconds to log.
- Review tab is data-driven — No AI generation. Just counts and grouped lists from the interaction log within the selected date range.
- Snapshots preserve history — When you save a review, the counts and date range are frozen so you can compare periods later.
- Soft delete for team members — Setting status to "former" preserves all history. No data loss.
- All routes require auth — Except /health and /api/auth/* routes.
- Category badges are consistent — Same colors everywhere (team-os counts, interaction list, review sections).