How I built a Jira alternative in Django — multi-tenancy, planning poker, and AI task generation
I've been running Scrum for small dev teams for years and kept hitting the same wall: Jira was overkill and expensive, simpler tools had no real Scrum support, and planning poker always needed a separate app that never s
I've been running Scrum for small dev teams for years and kept hitting the
same wall: Jira was overkill and expensive, simpler tools had no real Scrum
support, and planning poker always needed a separate app that never synced
back to the sprint board.
So I built SprintFlow — an agile sprint planning tool with built-in planning
poker, AI task generation, backlog management, and velocity tracking. Flat
team pricing, not per seat. Here's how I built it in Django.
The stack
- Django 6 + Python 3.14
- PostgreSQL
- Render (auto-deploy from GitHub)
- Anthropic SDK (
claude-sonnet-4-6) for AI features - Chart.js for velocity and burndown charts
- SortableJS for drag-and-drop backlog ordering
- Resend for transactional email
- Paddle for billing
No React, no microservices. Just Django serving HTML templates with
sprinkles of vanilla JS where needed.
Multi-tenancy architecture
Every model has an organization FK. Every view enforces org-scoping
via middleware that attaches the org and member to the request object:
class OrgMiddleware:
def __call__(self, request):
if request.user.is_authenticated:
try:
member = SprintMember.objects.select_related(
'organization', 'user'
).get(user=request.user)
request.org = member.organization
request.member = member
except SprintMember.DoesNotExist:
request.org = None
request.member = None
return self.get_response(request)
Every view then calls helper functions before touching any data:
def get_org(request):
if not request.org:
raise PermissionDenied
return request.org
def is_sm(request):
return request.member and request.member.role == 'scrum_master'
This means a developer can never accidentally leak data between orgs —
the org is always pulled from the authenticated session, never from a
URL parameter or request body.
Planning poker without WebSockets
Getting all team members to see votes reveal simultaneously was the
trickiest part. I didn't want the infrastructure overhead of WebSockets
for an MVP, so I went with a polling approach.
Each estimation session has a revealed boolean on the model. Members
poll every 2 seconds:
def session_status(request, session_id):
session = get_object_or_404(
EstimationSession,
id=session_id,
organization=get_org(request)
)
votes = session.votes.select_related('member__user').all()
return JsonResponse({
'revealed': session.revealed,
'vote_count': votes.count(),
'member_count': session.members.count(),
'votes': [
{
'member': v.member.display_name(),
'value': v.value if session.revealed else None,
}
for v in votes
]
})
When the Scrum Master reveals, revealed flips to True and the next
poll shows all votes simultaneously. Feels real-time for the estimation
use case without the complexity of maintaining WebSocket connections.
AI task generation with Anthropic
I use the Anthropic Python SDK for two AI features — task generation
(available to all members on Business plan) and story generation (Scrum
Master only).
The core function:
def _call_anthropic(prompt, system):
api_key = os.environ.get('ANTHROPIC_API_KEY', '')
if not api_key:
return None, 'ANTHROPIC_API_KEY not set'
try:
import anthropic
client = anthropic.Anthropic(api_key=api_key)
message = client.messages.create(
model='claude-sonnet-4-6',
max_tokens=1000,
system=system,
messages=[{'role': 'user', 'content': prompt}],
)
return message.content[0].text, None
except Exception as e:
return None, str(e)
For task generation, the system prompt instructs the model to return
structured JSON — a list of tasks with title, description, acceptance
criteria, estimated hours, and stream. I then parse and create Task
objects directly.
To prevent runaway costs, I track usage per org on the Subscription
model:
class Subscription(models.Model):
organization = models.OneToOneField(Organization, ...)
plan = models.CharField(...)
ai_calls_used = models.IntegerField(default=0)
def ai_calls_limit(self):
limits = {'starter': 0, 'pro': 0, 'business': 500}
if self.organization.is_test:
return 99999
return limits.get(self.plan, 0)
def can_use_ai(self):
return self.ai_calls_used < self.ai_calls_limit()
Every AI view checks can_use_ai() before calling the API and
increments ai_calls_used after a successful response.
Scrum role permissions
Only Scrum Masters can create and close sprints, run planning poker
sessions, generate AI stories, and access analytics. I enforce this
with a simple decorator:
def sm_required(view_func):
def wrapper(request, *args, **kwargs):
if not is_sm(request):
return JsonResponse(
{'error': 'Scrum Master access required'},
status=403
)
return view_func(request, *args, **kwargs)
return wrapper
What I learned
Multi-tenancy is the hardest part to get right. Not technically
hard, but requires discipline on every single query. I added a lint
step that flags any queryset not filtered by organization= — caught
several potential data leaks during development.
Polling works fine at small scale. For planning poker with teams
of 3–30 people, 2-second polling is imperceptible and dramatically
simpler than WebSockets. I'll revisit if I need sub-second updates
elsewhere.
Django is genuinely great for SaaS. The ORM, middleware, template
engine, and admin panel gave me everything I needed to build a
multi-tenant SaaS without touching a frontend framework. The codebase
is a single Django app — easy to reason about, easy to deploy.
Anthropic's SDK is clean to work with. The Python SDK is
straightforward and the model's structured output (when prompted
correctly) is reliable enough to parse directly into database objects
without defensive gymnastics.
Current status
The product is live at getsprintflow.co
with a 14-day free trial. 6 weeks post-launch, organic search
impressions are growing week over week and positions are climbing
across "sprint planning tool", "scrum tool", and "jira alternative"
queries.
Happy to answer questions on the Django architecture, AI integration,
multi-tenancy approach, or anything else.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.