workspace_id + slack_user_id scope every row. This eliminates an entire category of security risk (password storage, session hijacking) for free.services/. This keeps the code testable without mocking the Slack SDK.services/groq.service.ts, so retry logic, logging, and (future) model-swapping happen in exactly one place.docs/ROADMAP.md for the point at which each of these gets introduced).flowchart LR
subgraph Slack["Slack Workspace"]
U[User] -->|slash command / DM / channel message| Bolt
end
subgraph Backend["NEXUS Backend (Node.js / TypeScript)"]
Bolt[Slack Bolt App] --> Controllers
Controllers --> Services
Services --> Groq[Groq API\nLlama 3.3]
Services --> Supabase[(Supabase\nPostgres)]
Cron[node-cron jobs] --> Services
end
subgraph Frontend["Static Frontend (Vercel)"]
Landing[Landing Page]
Dashboard[Dashboard]
end
Dashboard -.->|future: authenticated REST API| Backend
Bolt -->|ephemeral / DM messages| U
This is the feature judges will remember, so its data flow is documented in detail.
sequenceDiagram
participant User
participant Slack
participant Bolt as Bolt App
participant Deadline as deadline.service.ts
participant Groq
participant DB as Supabase
User->>Slack: "Let's ship this by Friday"
Slack->>Bolt: message.channels event
Bolt->>Deadline: extractDeadline(text, today)
Deadline->>Deadline: cheap regex pre-filter
alt No temporal language
Deadline-->>Bolt: isDeadline=false (skip Groq call)
else Temporal language found
Deadline->>Groq: structured JSON extraction prompt
Groq-->>Deadline: {isDeadline, extractedTask, isoDate, confidence}
Deadline-->>Bolt: result
alt confidence >= 0.6
Bolt->>Slack: postEphemeral "Add as task?" (visible only to author)
User->>Slack: clicks "Add as task"
Slack->>Bolt: block_actions payload
Bolt->>DB: insert into tasks
Bolt->>Slack: postEphemeral confirmation
end
end
Why an ephemeral prompt, not an automatic task: false positives are inevitable with NLP. An ephemeral, one-click confirmation keeps the human in the loop without adding friction — the opposite of a bot that silently fills your task list with noise, and the opposite of one that interrupts a channel publicly.
sequenceDiagram
participant User
participant Bolt
participant MeetingPrep as meetingPrep.service.ts
participant Memory as memory.service.ts
participant Tasks as task.service.ts
participant Groq
User->>Bolt: /nexus prep "Acme renewal call"
Bolt->>MeetingPrep: prepareMeeting(userId, title)
par
MeetingPrep->>Memory: searchMemory(title)
and
MeetingPrep->>Tasks: listTasks(status=open)
end
MeetingPrep->>Groq: synthesize talking points + follow-ups (JSON mode)
Groq-->>MeetingPrep: {talkingPoints, followUps}
MeetingPrep-->>Bolt: MeetingPrepResult
Bolt->>User: formatted Slack message
erDiagram
WORKSPACES ||--o{ USERS : has
USERS ||--o{ TASKS : owns
USERS ||--o{ REMINDERS : owns
USERS ||--o{ MEMORY_ENTRIES : owns
USERS ||--o{ CONVERSATION_HISTORY : owns
WORKSPACES {
uuid id PK
text slack_team_id
text team_name
}
USERS {
uuid id PK
uuid workspace_id FK
text slack_user_id
text display_name
}
TASKS {
uuid id PK
uuid user_id FK
text title
text status
text priority
timestamptz due_date
}
REMINDERS {
uuid id PK
uuid user_id FK
text message
timestamptz remind_at
bool delivered
}
MEMORY_ENTRIES {
uuid id PK
uuid user_id FK
text content
text source_type
text[] keywords
}
CONVERSATION_HISTORY {
uuid id PK
uuid user_id FK
text role
text content
}
Full SQL: backend/src/database/schema.sql.
flowchart TB
subgraph Local["Local Dev"]
Dev[Developer] -->|Socket Mode, no public URL| SlackDev[Slack API]
end
subgraph Prod["Production"]
SlackAPI[Slack API] -->|HTTPS events| Railway[Backend host\ne.g. Railway / Render free tier]
Railway --> SupabaseProd[(Supabase Postgres)]
Railway --> GroqAPI[Groq API]
Vercel[Vercel: static frontend] -.->|future REST API| Railway
end
We use Socket Mode for local development (no tunneling/ngrok needed) and switch to HTTP mode with a public URL only in production — controlled entirely by the SLACK_SOCKET_MODE env var, no code changes required.
| Risk | Mitigation |
|---|---|
| Groq API rate limits under load | Central completeChat() wrapper with exponential backoff retry; cheap regex pre-filter avoids calling Groq on messages with no deadline signal |
| False-positive deadline detection annoying users | Ephemeral (private) prompts only, confidence threshold, one click to dismiss |
| Supabase free-tier connection limits | Single shared client (supabaseClient.ts), no per-request client creation |
| Conversation history growing unbounded | trim_conversation_history Postgres function called after each turn, keeps last 40 messages/user |
| Slack token leakage | Tokens only in .env (gitignored), never logged (logger.ts never receives raw tokens) |
| Reminder delivery drift | Cron polls every minute with a 1-minute lookahead window — acceptable precision for a “remind me” feature, not a scheduling engine |