Skip to content

About

A tool for planning poker exercises. Always free, without limits.

Topics

Resources

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

FreePlanningPoker.io

Website Docker Image Version

Fast and easy planning poker sessions for your whole team: free, secure, and open-source!

Always Free ♣ Unlimited Sessions ♠ Unlimited Participants

FreePlanningPoker.io • Deploy Yourself


This is Free Planning Poker, a free tool for software teams to do "planning poker" exercises to estimate the difficulty and length of development tasks. You can probably use it for other purposes if you need, too. It's always going to be free, without limits.

Running Locally

The ideal scenario is that you can "clone and go" without much (if any) work, but there's a couple steps you need right now:

  1. Fork and clone this repo
  2. Download and install the .NET 10 SDK
  3. I recommend using VSCode with the C# Dev Kit

You should be good to go now - hit F5 and watch it run! By default it will use an in-memory store to keep state. This store is not thread safe; in order to get thread safety (and to allow SignalR to use a backplane) you'll need to provide a connection string for Redis. However, the in-memory store is fast and ideal for local debugging scenarios.

Running with Redis

  1. You will need access to some deployment of Redis. Redis' Quick Start docs can help you here.
  2. Add your Redis connection string in appsettings:
"ConnectionStrings": {
    "Redis": "<your-connection-string>"
}

The application will see your connection string and use the Redis store instead of the in-memory store, and it will use Redis as a backplane for SignalR.

In future I want to look into Docker environments to be able to remove standing up your own Redis as being a burden.

Running with Docker

FreePlanningPoker comes with a standalone Dockerfile that you can run in Docker.

Deploying

FreePlanningPoker is published as a Docker image on Docker Hub. The container serves both the web client and the server on port 8080.

Note

End-to-end encryption relies on the browser's Web Crypto API, which is only available over HTTPS (or on localhost). Further, client<->server communication uses WebSockets. Ensure your network configurations support these.

Deploying the Image

docker run -d -p 8080:8080 --name free-planning-poker ianwold/free-planning-poker:latest

Then open http://localhost:8080.

Without any further configuration the app uses its in-memory store. That's fine for trying it out or for a small team on a single instance, but the in-memory store is not thread safe and it can't be shared between instances. All sessions are also lost whenever the container restarts. For anything more serious, run it with Redis.

Deploying with Redis

If you give the app a Redis connection string through the ConnectionStrings__Redis environment variable, it will use Redis to store sessions and as a SignalR backplane. This makes the app thread safe and lets you run several instances behind a load balancer.

If you already have a Redis instance:

docker run -d -p 8080:8080 \
  -e ConnectionStrings__Redis="<your-connection-string>" \
  --name free-planning-poker \
  ianwold/free-planning-poker:latest

Otherwise, you can run both together with Docker Compose. As an example, this is a minimal compose.yaml:

services:
  app:
    image: ianwold/free-planning-poker:latest
    ports:
      - "8080:8080"
    environment:
      ConnectionStrings__Redis: redis:6379
    depends_on:
      - redis
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    restart: unless-stopped

Then start it with:

docker compose up -d

The connection string follows the StackExchange.Redis configuration format, so you can add options like a password or TLS as needed (e.g. my-redis:6380,password=secret,ssl=true). Session data doesn't need to survive a Redis restart: every key has a 24-hour TTL, so you don't need to set up Redis persistence.

Health Checks

The server exposes two endpoints for your load balancer, orchestrator, or uptime monitor:

  • /health/live returns 200 whenever the server process is up and serving requests. It doesn't check Redis, so it's the one to use for restarting a stuck container.
  • /health/ready returns 200 when the server can reach Redis (it sends a PING with a 3-second timeout), and 503 otherwise. With the in-memory store it always returns 200. Use this one for routing traffic and for alerting.

Building the Image Yourself

If you've made changes of your own, you can build the image from the Dockerfile at the root of the repo:

docker build -t free-planning-poker .
docker run -d -p 8080:8080 free-planning-poker

Developing

The web client is a Blazor WASM SPA, the server is ASP and they communicate exclusively over SignalR (websockets). The server uses Redis as a backplane for SignalR and to store active sessions - this allows the server to scale horizontally.

architecture

Server

The SignalR communication is defined by two interfaces in the PlanningPoker project: IServer defines client-to-server communication (some of which does require a round trip) and IClient defines server-to-client communication (none of which requires a round trip; this must be asynchronous communication).

The logic for the server methods is in SessionHub in the Server project. This class contains the very minimal business rules and the scheme of notifying clients of changes through IClient. Clients are grouped by session id, and only clients in a session will receive notifications for it.

State is kept by one of the two classes implementing IStore: either InMemoryStore or RedisStore. The former is used for local debugging scenarios where Redis isn't strictly needed, while the latter is used for production deployments and any networking-related debugging and testing.

If you are adding a method on the server for the client to call, you'll update IServer, implement the server logic in SessionHub and the store classes, then you'll update the client's Client class to call it (see below). If you're adding a method on the client to call, you'll update IClient, implement the client logic in Client and SessionStore (see below), then you'll update the server's SessionHub to call down through that method. Everything is strongly-typed by these interfaces on both the client and server, keeping you from needing to using magic strings.

Configuration and dependency injection are all set up in Program; there's really not a lot there.

Redis

Session data is stored in Redis across several keys to eliminate or minimize race conditions. The keys and their values are:

  • {sessionId} (guid): Hash with values "Title" and "State".
  • {sessionId}:points: List with values being the point options available in the session.
  • {sessionId}:participants: List with values being the IDs of the participants in the session.
  • {sessionId}:participants:{participantId}: Hash with values "Name", "Points", and "Stars".

All entries associated with a session are removed from Redis when the last participant leaves the session. For extra safety, the keys all have a 24-hour TTL.

Client

The Client is separated into two pieces - a logical client in PlanningPoker.Client and the web (Blazor) view in PlanningPoker.Client.Web. The logical client is split by responsibility rather than one big class:

  • SessionStore holds the session data a UI needs to render - the current Session, Self/Others, ShowShareNotification, etc. - and raises Changed whenever any of it updates. Nothing outside PlanningPoker.Client can write to it directly; its state only changes through its own methods.
  • ToastStore holds the transient notification toasts shown in the UI, independently of session data, with its own Changed event.
  • Client is the piece that actually knows about SignalR - it implements IClient, keeps an instance of IServer, and exposes the methods a UI calls to act (CreateAsync, UpdatePoints, etc). It writes into SessionStore/ToastStore as a side effect of handling server callbacks or fulfilling those calls, but exposes nothing to read itself - components should consult the stores for state and call Client only to perform actions.

Platform-specific behavior (building the hub URL, what to do when a session is created or closed, any JS interop needed at startup) is captured by the ISessionTransport interface, which Client takes as a constructor dependency rather than through subclassing. This keeps Client itself free of any browser-specific dependency and lets a different UI (or the integration tests) supply its own transport.

Client's connection is set up in EnsureInitialized, and torn down in LeaveAsync. When adding server functionality, you'll typically only need to change Client (for the SignalR/actions side) and SessionStore (if the change adds or updates data a UI needs to read) unless it also requires new UI components.

On the Blazor client, there's two main files to care about: WebSessionTransport (the browser's ISessionTransport implementation) and SessionPage.

SessionPage is the user interface for almost the entire application. The user will first create a session on the homepage (Index.razor) but then all the work in the session is done on this page. Components inject SessionStore (and ToastStore, where relevant) to read state and subscribe to Changed, and inject Client separately to perform actions. Several components for the UI are broken out into separate Razor components in the Components directory.

Dark/light color mode is handled directly through JS in index.html. E2E encryption is implemented there too, but Client never calls into JS for it directly - it depends on the IEncryptionService interface, which EncryptionService implements via JS interop. This keeps the logical client itself free of any browser-specific dependency, so a non-Blazor client could supply its own IEncryptionService instead. I wrote a blog post on E2E encryption which covers the implementation used here.

Testing

Integration tests live in PlanningPoker.IntegrationTests. They deliberately drive the app only through Client, SessionStore, and ToastStore (via a TestSessionTransport implementation of ISessionTransport, the same pattern WebSessionTransport follows) rather than reaching into SessionHub or the store classes directly - the goal is to test the behavior a participant actually sees, so the suite doesn't need to change every time something internal gets refactored.

Each behavior is written once and run in up to three modes: against InMemoryStore, against RedisStore with a single server instance, and (for tests with more than one participant) against RedisStore with two server instances sharing one Redis, to prove the SignalR backplane actually relays messages between them. The Redis-backed runs spin up a real Redis container via Testcontainers, so you'll need Docker running locally to execute the full suite - dotnet test will otherwise fail on those specific tests while the InMemoryStore ones still pass fine.

Contributing

Please do! I think the above gives a fair quick overview of the project structure and how to add some features. I've got several good first issues and I'm always happy to discuss suggestions for what to include, modify, etc.

If you would like to champion an issue, please leave a comment saying you'd like to - I'll assign the issue to you and I'll be happy to clarify any questions.

I don't have formal code standards on this proejct yet; it's quite small and young. I ask that your code be kept minimal, tidy, and in-keeping with the code that's already here. In future as the application solidifies then a more defined coding and architectural standard will probably emerge - I find that a codebase will generally reveal its own standards over time and I prefer allowing that process rather than imposing a (probably wrong) idea on the codebase from the start.

About

A tool for planning poker exercises. Always free, without limits.

Topics

Resources

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages