Task management API built with Node.js, Express, Sequelize, and PostgreSQL.
- Node.js 20+
- pnpm
- PostgreSQL
git clone https://github.com/realabdullah/taskgid-api.git
cd taskgid-api
pnpm install
cp .env.example .env
Fill in .env. DATABASE_URL wins over the discrete DB_* variables when
both are set (see src/config/config.cjs). Every other variable is optional —
the feature it configures is disabled without it, not broken.
Create the database, then apply migrations:
createdb taskgid
pnpm db:migrate
pnpm dev # development, with reload
pnpm start # production
Migrations in migrations/ are the source of truth. They run before the
server starts on deploy (see the CMD in Dockerfile) — sequelize.sync()
creates missing tables but never missing columns, so booting first would fail
on the first query against a new one.
pnpm db:migrate # apply
pnpm db:migrate:down # roll back the last migration
scripts/migration-helpers.cjs provides addColumnIfMissing,
createTableIfMissing, and similar guards; new migrations should use them
rather than a bare addColumn/createTable, since this database predates
migrations and may already have the object.
pnpm db:sync / pnpm db:sync:force sync models directly for local
development. :force drops and recreates every table.
src/services/workspaceEvents.js publishes task.created, task.updated,
task.deleted, and comment.created to Pusher on the private-workspace-{id}
channel. POST /api/pusher/auth authorizes subscriptions against workspace
membership. Without PUSHER_* credentials configured, publishing is a no-op.
PUSHER_HOST, PUSHER_PORT, and PUSHER_USE_TLS point the client at any
Pusher-protocol-compatible server instead of Pusher's own.
The same four events also persist to workspace_events, in the same
transaction as the row change they describe, and queue a delivery for every
active endpoint a workspace has configured. The first attempt fires
synchronously, right after that transaction commits; a failed attempt retries
on the backoff schedule in src/services/webhookService.js, driven by
pnpm webhooks:retry.
Endpoint URLs must be https and cannot point at a private, loopback, or
link-local address — a workspace member's webhook URL is a server-side
outbound request target, so this only guards against the address given at
creation time, not one a hostname later resolves to.
Deliveries are signed: X-Taskgid-Signature: t=<unix seconds>,v1=<hex>, where
the hex value is HMAC-SHA256(secret, "{timestamp}.{body}"). X-Taskgid-Delivery
carries a stable id across retries of the same delivery, for deduplication.
Managed at /workspaces/:workspaceSlug/webhooks, admin or creator only:
GET /— list endpoints, without secretsPOST /— create; response carries the signing secret oncePATCH /:id— update url, description, subscribed events, or active statePOST /:id/rotate-secret— issue a new secret, invalidating the old oneDELETE /:id— remove, along with its delivery historyGET /:id/deliveries— paginated delivery log
| Job | Command | Schedule |
|---|---|---|
| Digest emails | pnpm digests:send |
Not currently scheduled |
| Recurring task spawner | pnpm recurrences:spawn |
.github/workflows/spawn-recurrences.yml, daily |
| Webhook delivery retries | pnpm webhooks:retry |
.github/workflows/retry-webhook-deliveries.yml, manual trigger only |
Each reads due work based on its own time zone or watermark, so a run that happens late catches up and a run that happens twice is a no-op.
These workflows need DATABASE_URL on the Production GitHub environment;
a job must declare environment: Production to receive it.
GET /calendar/:token.ics serves one user's tasks with a due date as a
read-only iCalendar feed. The token is the credential; only its hash is
stored. Manage it while authenticated:
GET /users/calendar-feed→{enabled}POST /users/calendar-feed→ generates or rotates the token, returning{url}onceDELETE /users/calendar-feed→ revokes it
PUBLIC_API_URL sets the origin used to build the feed URL.
A key authenticates as its issuer, with that user's role, but only within the
one workspace it was created in — a key for workspace A is rejected against
workspace B even if the same user belongs to both. Send it the same way as a
session token: Authorization: Bearer tg_key_....
Managed at /workspaces/:workspaceSlug/api-keys, by any member for
themselves:
GET /— an admin or the creator sees every key; anyone else sees only their ownPOST /— create; response carries the raw key onceDELETE /:id— revoke; the owner or a workspace admin can revoke any key
Outbound workspace events to a Slack channel, plus Mark done / Claim buttons
on those messages. Requires a Slack app with bot scopes chat:write,
channels:read, groups:read, users:read, and users:read.email. Set
SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, and SLACK_SIGNING_SECRET. Point
the app's redirect URL at {PUBLIC_API_URL}/slack/oauth/callback and its
interactivity request URL at {PUBLIC_API_URL}/slack/interactions.
Without those credentials the management endpoints report configured: false
and OAuth is refused; event publishing is a no-op when no installation is
active.
Managed at /workspaces/:workspaceSlug/slack, admin or creator only:
GET /— current installation (without the bot token) andconfiguredPOST /connect— returns the Slack authorize{url}GET /channels— channels the bot can seePATCH /— setchannelId,eventTypes, orisActiveDELETE /— disconnect
Public callbacks (no session auth; Slack signs interactivity):
GET /slack/oauth/callback— finishes install, redirects to settingsPOST /slack/interactions— button actions
POST /mcp is a Model Context Protocol
Streamable HTTP endpoint. Clients authenticate in one of two ways:
- OAuth 2.1 (Claude.ai and other remote MCP clients) — discovery at
/.well-known/oauth-authorization-server, dynamic client registration at/register, authorization code + PKCE at/authorizeand/token. The user signs in at/mcp/oauth/consentand picks a workspace. No static Client ID or secret is required; leave those fields empty in Claude. - Workspace API key —
Authorization: Bearer tg_key_…for clients that only support a bearer header (for example Cursor with a pasted key).
Session JWTs are rejected: credentials must pin the call to one workspace.
PUBLIC_API_URL must be the public HTTPS origin of this API so issuer and
redirect URLs match what clients call.
Tools (each enforces the actor's membership and role the same way the REST routes do):
| Tool | REST equivalent |
|---|---|
list_tasks |
GET /workspaces/:slug/tasks |
search_tasks |
GET /workspaces/:slug/tasks/search |
create_task |
POST /workspaces/:slug/tasks |
update_task |
PATCH /workspaces/:slug/tasks/:id |
add_comment |
POST /workspaces/:slug/tasks/:id/comments |
get_workspace_summary |
GET /workspaces/:slug/statistics |
Mutations write TaskActivity rows with source: agent so the audit trail
distinguishes agent actions from interactive ones.
- In-app / push — Novu, configured via
NOVU_API_KEY. - Realtime — Pusher, see above.
- Email — ZeptoMail first, falling back to Resend, via
ZEPTO_MAIL_TOKEN/RESEND_API_KEY. - Slack — see above.
/api-docs serves swagger-output.json, generated from the actual routes by
pnpm docs:generate. openapi.yaml holds the shared schemas, parameters, and
security schemes the generator reuses — including apiKeyAuth, for
authenticating as a workspace-scoped API key instead of a session — and does
not itself list every path. Run pnpm docs:generate after adding or changing
a route, and commit the result.
ISC