AsyncStandUp

A self-hosted async standup tool for distributed teams — no Slack, no Azure, no subscriptions. Just PHP, a database, and your own server.

PHP 8.3+ SQLite / MySQL / PostgreSQL 127 tests PHPStan level 5 MIT License

Overview

AsyncStandUp replaces synchronous stand-up meetings with a scheduled async workflow. Each morning, the application emails (or Teams-messages) every developer with a set of team-specific questions. Developers respond via a web form or directly in a Teams Adaptive Card. The application collects all responses, flags blockers, tracks mood, and sends a daily summary to the team.

Everything runs from a single PHP application. No cloud vendor lock-in. No monthly subscriptions. The only external dependencies are an SMTP server and — optionally — a Microsoft Teams webhook or bot registration.

AsyncStandUp Dashboard

The main dashboard shows all teams, today's participation status, and quick actions.

Features

🏢
Multi-org, multi-team Organisations contain teams; teams have members, questions, and schedules.
📅
Configurable frequency Daily, weekdays only, or weekly on a chosen day.
⏰
Per-team timezone & schedule Standup time and summary time set independently per team.
🔔
Submission reminders Automatic reminder to members who haven't responded by a threshold time.
⚠️
Blocker flagging One question per team marked as a blocker question — prefixed in summaries.
😊
Mood / sentiment tracking 5-level mood score per submission; 30-day trend on the dashboard.
💬
MS Teams integration DM prompts with inline answer cards, channel summaries, 3 modes.
🔌
Public REST API API key auth, rate-limited, 5 endpoints for reading and submitting standups.
🤖
MCP Server stdio JSON-RPC 2.0 — connect directly from Claude Desktop or Pi SDK.
🗝️
API key management Generate, list, revoke API keys from the settings UI. Soft-delete with audit trail.
🔒
Invitation-based membership Team owners invite members via email. Admin approves registrations.
🗄️
Multi-database support SQLite for simple deployments, MySQL or PostgreSQL for production scale.

Requirements

ComponentMinimumNotes
PHP8.3PDO, mbstring, openssl, json extensions required
DatabaseSQLite 3 / MySQL 5.7+ / PostgreSQL 13+SQLite is zero-config for small teams
SMTPAny SMTP serverRequired for email prompts and summaries
Web serverApache / nginx / PHP built-inmod_rewrite for API + bot webhook clean URLs
HTTPSOptional for email mode; required for Teams bot DMsLet's Encrypt is free
Croncrontab access2 jobs, each run every minute

Installation

1. Clone the repository

git clone https://github.com/cedric-r/asyncstandup.git
cd asyncstandup

2. Configure the application

cp config/config.example.php config/config.php
# Edit config/config.php with your database, SMTP, and app settings

3. Create the database

SQLite (simplest):

# Ensure the data/ directory is writable
mkdir -p data
chmod 750 data
# Schema is applied automatically on first run, or run manually:
php -r "require 'src/Db.php'; getDb(require 'config/config.php');"

MySQL / PostgreSQL:

# Create the database, then apply the schema:
mysql -u root -p asyncstandup < db/schema.sql
# or
psql -U postgres asyncstandup < db/schema-postgresql.sql

4. Set up cron jobs

# Run every minute — each script checks per-team time + timezone on every run
* * * * *  www-data  php /var/www/asyncstandup/cron/send_standups.php  >> /var/log/asyncstandup.log 2>&1
* * * * *  www-data  php /var/www/asyncstandup/cron/send_summaries.php >> /var/log/asyncstandup.log 2>&1

5. Run locally (for testing)

php -S localhost:8080 -t public/
# Open http://localhost:8080

Configuration

All configuration lives in config/config.php. Copy from config/config.example.php and edit:

<?php
return [
    // ── Database ──────────────────────────────────────────────
    'db' => [
        'driver'   => 'sqlite',        // 'sqlite' | 'mysql' | 'pgsql'
        'path'     => __DIR__ . '/../data/asyncstandup.db',  // SQLite only
        'host'     => 'localhost',     // MySQL/PostgreSQL
        'port'     => 3306,
        'name'     => 'asyncstandup',
        'user'     => 'dbuser',
        'password' => 'secret',
    ],

    // ── SMTP ──────────────────────────────────────────────────
    'smtp' => [
        'host'       => 'smtp.example.com',
        'port'       => 587,
        'encryption' => 'tls',         // 'tls' | 'ssl' | ''
        'username'   => 'user@example.com',
        'password'   => 'smtp-password',
        'from_email' => 'standup@example.com',
        'from_name'  => 'AsyncStandUp',
    ],

    // ── Application ───────────────────────────────────────────
    'app' => [
        'url'      => 'https://standup.example.com',
        'timezone' => 'Europe/Paris',
        'debug'    => false,
    ],

    // ── MS Teams Bot (optional) ───────────────────────────────
    'teams_bot' => [
        'app_id'           => 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
        'app_secret'       => 'your-bot-app-secret',
        'service_url'      => 'https://smba.trafficmanager.net/emea/',
        'bot_webhook_path' => '/bot/webhook',
    ],
];

Organisations & Teams

The hierarchy is: Organisation → Team → Members → Questions. An admin creates an organisation, then creates teams within it. Team owners manage their own members and questions.

Team Settings

Team Settings — MS Teams mode selector

Team edit page showing the notification mode selector with Teams configuration fields.

Questions

Each team defines its own standup questions. Questions are ordered (drag to reorder), and each can be flagged as a blocker question (⚠️) or a mood question (😊). Only one blocker and one mood question per team.

Team Questions page

Question management — toggle blocker and mood flags per question.

Submitting Standups

Developers receive a prompt each morning (email or Teams DM). They click the link and fill in their standup on a simple form. Each token is single-use and expires after the team's standup window.

If the team uses Teams DM mode, the Adaptive Card has inline text inputs — answers are submitted without leaving Teams.

Response Browser

Response Browser

Browse all standup responses — filter by date, member, or team. Blocker answers are highlighted in red.

Team Dashboard

Team Dashboard — Engineering

Engineering team dashboard showing today's submissions, participation history, and mood trend.

Team Dashboard with Mood Tracking

Dashboard with 30-day mood tracking. The weekly dip on Wednesdays and peak on Fridays is clearly visible.

Mood Tracking

Enable mood tracking by marking one question as the mood question (😊 toggle on the Questions page). Developers answer with a number 1–5 in free text (e.g. "4 — feeling good!"). The application extracts the score automatically.

  • 1 — Not great
  • 2 — Below average
  • 3 — Okay
  • 4 — Good
  • 5 — Excellent

The 30-day trend chart on the dashboard and the average score in daily summaries both draw from these scores.

Blocker Flagging

Mark one question as the blocker question (⚠️ toggle). Non-empty answers to this question are:

  • Highlighted in red in the response browser
  • Prefixed with ⚠️ in email and Teams summaries
  • Shown with a "BLOCKER" badge in the daily summary email

Submission Reminders

Reminders are handled automatically by send_standups.php on every run — members who have not submitted by the configured reminder window receive a nudge using their existing token. No separate cron entry needed.

Configurable Frequency

ModeBehaviour
dailyRuns every day including weekends
weekdaysRuns Monday–Friday; skips Saturday and Sunday
weeklyRuns once per week on the configured day (e.g. Monday)

MS Teams Integration

AsyncStandUp supports three notification modes. Each team picks one independently:

ModePrompt deliveryAnswer methodSummaryAzure needed
Email (existing)Web form linkEmailNone
teams-summary EmailWeb form linkTeams channel (Adaptive Card)None — webhook only
teams Teams DM (Adaptive Card)Directly in TeamsTeams channel (Adaptive Card)Bot registration (free)
ℹ️
Graceful degradation If a Teams delivery fails (network error, bad credentials), the system automatically falls back to email and logs the error. Teams is an enhancement layer — the application always works.

Channel Summaries (no Azure required)

  1. In your Teams channel → Apps → Connectors → Incoming Webhook → Create
  2. Copy the webhook URL
  3. In team settings, select Teams Channel Summary only and paste the URL

That's it. The daily summary posts to the channel as an Adaptive Card with all member responses.

Bot DM Prompts (bot registration required)

  1. Go to Azure Portal → Bot Services → Create → Free tier
  2. Note the App ID and create a client secret (App Secret)
  3. Set the messaging endpoint to https://your-server.com/bot/webhook
  4. Add to config/config.php:
    'teams_bot' => [
        'app_id'           => 'your-app-id',
        'app_secret'       => 'your-app-secret',
        'service_url'      => 'https://smba.trafficmanager.net/emea/',
        'bot_webhook_path' => '/bot/webhook',
    ],
  5. In team settings, select Teams DM + Channel Summary
⚠️
Production deployment note The bot webhook JWT validation does not verify the RS256 cryptographic signature in the current version (adequate for dev/internal). Add JWKS-backed signature verification before exposing the bot webhook to the public internet. See public/bot/webhook.php for the TODO.
Admin Teams Integration page

The admin Teams Integration page shows each team's notification mode, webhook URL, and any recent delivery errors.

REST API

Base URL: https://your-server.com/api/v1/

All requests require an API key in the Authorization header:

Authorization: Bearer sk-<your-64-char-key>

Rate limit: 100 requests per hour per API key. Exceeding returns HTTP 429 with a Retry-After header.

GET /api/v1/teams List teams you are a member of
curl -H "Authorization: Bearer sk-..." https://your-server.com/api/v1/teams
GET /api/v1/teams/{id}/questions Get questions for a team
GET /api/v1/teams/{id}/submissions List submissions (paginated)
# Optional query params: ?date=2026-08-18&page=1&per_page=20
GET /api/v1/submissions/{id} Get a single submission with all answers
POST /api/v1/teams/{id}/submissions Submit a standup programmatically
curl -X POST \
  -H "Authorization: Bearer sk-..." \
  -H "Content-Type: application/json" \
  -d '{
    "answers": {
      "1": "Fixed the login bug",
      "2": "Writing tests for the auth module",
      "3": ""
    }
  }' \
  https://your-server.com/api/v1/teams/1/submissions

MCP Server

AsyncStandUp ships with a Model Context Protocol server. Connect it to Claude Desktop or any MCP-compatible AI client to query and submit standups by natural language.

Starting the server

ASYNCSTANDUP_API_KEY=sk-your-key php mcp/server.php

Claude Desktop configuration

{
  "mcpServers": {
    "asyncstandup": {
      "command": "php",
      "args": ["/path/to/asyncstandup/mcp/server.php"],
      "env": {
        "ASYNCSTANDUP_API_KEY": "sk-your-key"
      }
    }
  }
}

Available tools

list_teams
List all teams you are a member of
list_questions
Get the standup questions for a team
get_submissions
List standup submissions for a team (filterable by date)
get_submission
Get a single submission with all answers
submit_standup
Submit a standup for today
get_team_stats
Get participation and mood statistics for a team

Admin Panel

Admin Users page

User administration — approve registrations, manage roles, see last login.

Admin Teams Integration page

Teams Integration admin — mode badges, webhook URLs, last error with timestamp.

API Key Management

Each user manages their API keys from Profile → API Keys. Keys are:

  • Generated with bin2hex(random_bytes(32)) — 64-char hex, shown once only
  • Stored as a SHA-256 hash — the plain key is never stored
  • Soft-deleted on revoke (revoked_at timestamp) — audit trail preserved
API Keys Management

API key management — generate named keys, view masked preview, revoke individually.

Cron Jobs

Both scripts run every minute. Each team has its own standup time and timezone configured in Team Settings. Deduplication guards prevent double-sending.

ScriptPurposeSchedule
cron/send_standups.php Standup prompts + reminders (per-team timezone check) * * * * *
cron/send_summaries.php Daily summaries (per-team summary time check) * * * * *
# /etc/crontab
* * * * *  www-data  php /var/www/asyncstandup/cron/send_standups.php  >> /var/log/asyncstandup.log 2>&1
* * * * *  www-data  php /var/www/asyncstandup/cron/send_summaries.php >> /var/log/asyncstandup.log 2>&1
ℹ️
Teams with frequency = weekdays are automatically skipped on weekends by the application. Teams with frequency = weekly are only triggered on their configured day.

Database

BackendSchema fileNotes
SQLitedb/schema.sql (used via PHP PDO)Zero-config; suitable for single-server deployments
MySQL 5.7+db/schema.sqlGood for multi-server or high-load deployments
PostgreSQL 13+db/schema-postgresql.sqlFull ACID compliance; recommended for production

Switch databases by changing the db.driver key in config/config.php. All three backends use the same application code — no ORM, plain PDO with parameterised queries.

Security Notes

AreaStatusNotes
Bot webhook JWT ⚠️ Partial aud/iss/exp validated; RS256 signature not verified. TODO before production go-live.
Bot token cache ✅ Restricted Cached to sys_get_temp_dir() with chmod 0600.
API keys ✅ SHA-256 hash only Raw key displayed once at generation; never stored in plaintext.
Teams webhook SSRF ✅ Mitigated Webhook URLs validated: HTTPS-only, FILTER_VALIDATE_URL, str_starts_with check. Bot serviceUrl restricted to known Bot Framework domains.
SQL injection ✅ Parameterised All queries use PDO prepared statements throughout.
XSS ✅ Encoded All user-supplied output through htmlspecialchars(ENT_QUOTES, 'UTF-8').
CSRF ✅ Token validation All POST forms include and validate a CSRF token.
Rate limiting ✅ API only 100 requests/hour per API key; 429 with Retry-After on breach.

Test Suite

php php83/php.exe tests/phpunit.phar --configuration tests/phpunit.xml
MetricValue
Tests127
Assertions255
PHPStan level5 (0 errors)
CoverageAll src/ classes; all DB schemas; all API endpoints

Tests use a fresh SQLite in-memory database per test class. No external services, no mocking of HTTP calls — all tests run against real production functions.

What Was Built (Story History)

StoryFeature
US-1–10Core: teams, members, questions, email prompts, submissions, summary emails, PHPUnit suite
US-11–20Organisations, invitations, admin panel, response browser, dashboard, PDF reports
US-21–28API key auth, consensus alerting, member history, team hardening
US-29Submission reminders
US-30Configurable standup frequency
US-31+32Blocker flagging + mood tracking
US-33Public REST API (5 endpoints, API key auth, rate limiting)
US-34MCP Server (stdio, 6 tools)
US-35API key management UI (generate, list, revoke, soft-delete)
US-36MS Teams schema + per-team mode selector
US-37Teams channel summary via Incoming Webhook + Adaptive Card
US-38Bot DM prompts (proactive Adaptive Card with inline answer fields)
US-39Bot webhook endpoint — card submission handler, replay guard, confirmation DM
US-40Teams fallback error tracking + admin visibility page