An incident management platform for tracking, triaging, and resolving incidents. Firetower provides severity and status classification, participant management, milestone tracking, and integrations with Slack.
- Backend: Django 5.2, Django REST Framework, PostgreSQL
- Frontend: React 19, TypeScript, TanStack Router/Query, StyleXJS, Radix UI
- SDK: Python SDK with JWT auth for programmatic access
- Tooling: uv (Python), pnpm (JS), Docker Compose
cp config.example.toml config.toml
# Edit config.toml with your values
docker compose -f docker-compose.db.yml up -d
uv run manage.py migrateYou can inspect the database with Adminer at http://localhost:8089.
uv sync
uv run manage.py runserverThe API will be available at http://localhost:8000.
cd frontend
pnpm install
pnpm devThe dev server will be available at http://localhost:5173.
Firetower's Slack integration uses Socket Mode, so no public URL or tunnel is needed for local development.
- Go to api.slack.com/apps and choose Create New App > From a manifest.
- Select your development workspace.
- Paste the contents of
slack-app-manifest.yaml, using the# test:values commented beside each field (e.g. nameFiretower Test, command/ft-test). - Install the app to your workspace when prompted.
From your app's settings page, grab two tokens and add them to config.toml:
| Token | Where to find it | config.toml key |
|---|---|---|
Bot token (xoxb-...) |
OAuth & Permissions > Bot User OAuth Token | slack.bot_token |
App-level token (xapp-...) |
Basic Information > App-Level Tokens (create one with the connections:write scope) |
slack.app_token |
Also set slack.team_id to your workspace's team ID (visible in the workspace URL or via the Slack API).
Create or choose a Slack channel for incident notifications and set its ID as slack.incident_feed_channel_id in config.toml. Invite the bot to that channel.
With the database and backend already running:
uv run manage.py run_slack_botThe bot connects via WebSocket and registers the /ft-test and /testinc slash commands. Type /ft-test help in Slack to verify.
# Backend
uv run pytest
# Frontend
cd frontend
pnpm testPre-commit is set up to handle all linting and formatting automatically. Install the hooks with:
uv run pre-commit installThis runs ruff, mypy, eslint, and prettier on commit, with knip on push.
All configuration lives in config.toml (copy config.example.toml as a starting point). Sections include:
- postgres: Database connection
- slack: Slack bot token and team config
- auth: IAP authentication
- datadog: Datadog metrics and event logging
src/firetower/ # Django backend
incidents/ # Core incident models and API
integrations/ # Slack
auth/ # Authentication
frontend/ # React frontend
sdk/ # Python SDK
Set action_item_label_id in the [linear] section of config.toml to the workspace-wide incident-action label's ID to enable automatic labeling during action item sync, including pre-existing issues adopted as follow-ups. Other labels are preserved. Labeling is skipped when the value is empty or omitted.
Scheduled tasks are stored as database objects and are managed via migrations.
First, define you task in the SCHEDULES map in src/firetower/incidents/tasks.py.
Then, create a new migration referencing it:
from django.db import migrations
from firetower.incidents.tasks import SCHEDULES
def create_schedule(apps, schema_editor):
Schedule = apps.get_model("django_q", "Schedule")
schedule_name = "[schedule name goes here]"
Schedule.objects.get_or_create(
name=schedule_name, defaults=SCHEDULES[schedule_name]
)
def delete_schedule(apps, schema_editor):
Schedule = apps.get_model("django_q", "Schedule")
schedule_name = "schedule_demo"
Schedule.objects.filter(name=schedule_name).delete()
class Migration(migrations.Migration):
dependencies = [
("incidents", "[previous migration goes here]"),
]
operations = [
migrations.RunPython(create_schedule, delete_schedule),
]First, generate the opposite of the migration used to add the schedule:
from django.db import migrations
from firetower.incidents.tasks import SCHEDULES
def create_schedule(apps, schema_editor):
Schedule = apps.get_model("django_q", "Schedule")
schedule_name = "[schedule name goes here]"
Schedule.objects.get_or_create(
name=schedule_name, defaults=SCHEDULES[schedule_name]
)
def delete_schedule(apps, schema_editor):
Schedule = apps.get_model("django_q", "Schedule")
schedule_name = "schedule_demo"
Schedule.objects.filter(name=schedule_name).delete()
class Migration(migrations.Migration):
dependencies = [
("incidents", "[previous migration goes here]"),
]
operations = [
migrations.RunPython(delete_schedule, create_schedule),
]Once this migration has run everywhere, you can then remove the body from the SCHEDULES array, but keep the key since these are still referenced in legacy migrations!
SCHEDULES = {
"schedule_demo": {
# Removed in <migration name>
},
}