DEV Community

Husam Alhwadi
Husam Alhwadi

Posted on

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 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)
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
        ]
    })
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)