README.md

July 10, 2026 · View on GitHub

Massive Nerds

RetroMatic, a real-time retrospective tool.

Build Status npm Twitter Ko-fi


RetroMatic is used for real-time retrospectives. See how to leverage Angular and Firebase for syncing data across all the participants. Retrospective's are held at the end of sprints in Agile software development. The team reflects on what happened in the sprint and determines actions for improvement.

Local Development

To clone and run this application locally, you'll need Git and Node.js 20+ (which comes with npm) installed on your computer.

Create a free Firebase project. In the Firebase console, enable Email/Password, Google, and Anonymous sign-in under the Sign-in method tab of the Auth section, and create a Realtime Database.

Create a .env file at the repo root with your project's config (find these under Firebase Console > Project settings > General > Your apps):

FIREBASE_API_KEY=
FIREBASE_AUTH_DOMAIN=
FIREBASE_DATABASE_URL=
FIREBASE_PROJECT_ID=
FIREBASE_STORAGE_BUCKET=
FIREBASE_MESSAGING_SENDER_ID=
npm install --legacy-peer-deps

npm run config -- --environment=prod
npm start

Note: npm run config with no flags currently does nothing useful for local dev (it's a known bug — see issue #77). Passing --environment=prod is what actually generates the src/environments/environment.ts file that npm start reads; it does not put the app in production mode. This will be simplified once #77/#78 are fixed.

Firebase Structure

$ are Firebase-generated unique IDs.

├── retroboards
   └── $retroboardId
       ├── creator (username)
       ├── creatorId ($userId)
       ├── noteCount
       ├── dateCreated
       ├── name
       └── timeZone
├── buckets
   └── $bucketId
       ├── retroboardId ($retroboardId)
       ├── creator (username)
       ├── creatorId ($userId)
       └── name
├── notes
     └── $noteId
         ├── creator (username)
         ├── creatorId ($userId)
         ├── retroboardId ($retroboardId)
         ├── bucketId ($bucketId)
         ├── message
         ├── voteCount
         └── votes
             └── $userId
├── users
   └── $userId
       ├── favorites
   └── $retroboardId (true, or absent if unfavorited)
       ├── md5hash
       └── displayName
└── bucketTemplates
    └── $templateId
        ├── name
        ├── bucketNames (array of strings)
        └── creatorId ($userId)

Users can save a reusable set of bucket names as a template from the create-retro dialog; templates are listed there for quick reuse on future boards.

Firebase Security Rules

These rules are currently documentation only — they aren't version-controlled or auto-deployed yet (tracked in issue #86). Treat the Firebase console as the source of truth for what's actually enforced in production, and update this section if you change it there.

{
  "rules": {
    "retroboards": {
      ".read": "auth != null",
      ".indexOn": ["creatorId"],
      "$retroboardId": {
        ".write": "(auth != null && !data.exists()) || data.child('creatorId').val() === auth.uid",
        ".validate": "newData.hasChildren(['creator', 'creatorId', 'noteCount', 'dateCreated', 'name', 'timeZone'])",
        "creator": {
          ".validate": "newData.isString()"
        },
        "creatorId": {
          ".validate": "auth.uid === newData.val() && root.child('users/' + newData.val()).exists()"
        },
        "noteCount": {
          ".validate": "newData.isNumber()"
        },
        "dateCreated": {
          ".validate": "newData.isString()"
        },
        "name": {
          ".validate": "newData.isString()"
        },
        "timeZone": {
          ".validate": "newData.isString()"
        }
      }
    },
    "notes": {
      ".read": "auth != null",
      ".indexOn": ["bucketId", "retroboardId"],
      "$noteId": {
        ".write": "(auth != null && !data.exists()) || (data.child('creatorId').val() === auth.uid || root.child('retroboards/' + data.child('retroboardId').val()).child('creatorId').val() === auth.uid)",
        ".validate": "newData.hasChildren(['creator', 'creatorId', 'retroboardId', 'bucketId', 'message', 'voteCount'])",
        "creatorId": {
          ".validate": "auth.uid === newData.val()"
        },
        "votes": {
          ".write": "auth != null"
        },
        "voteCount": {
          ".write": "auth != null"
        }
      }
    },
    "buckets": {
      ".read": "auth != null",
      ".indexOn": ["retroboardId"],
      "$bucketId": {
        ".write": "(auth != null && !data.exists()) || data.child('creatorId').val() === auth.uid",
        ".validate": "newData.hasChildren(['creator', 'creatorId', 'retroboardId', 'name'])",
        "creatorId": {
          ".validate": "auth.uid === newData.val()"
        }
      }
    },
    "users": {
      "$userId": {
        ".read": "$userId === auth.uid",
        ".write": "$userId === auth.uid",
        ".validate": "newData.hasChildren(['displayName', 'md5hash'])",
        "displayName": {
          ".validate": "newData.isString()"
        },
        "md5hash": {
          ".validate": "newData.isString()"
        }
      }
    },
    "$other": { ".validate": false }
  }
}

Firebase Authentication

To set up users, from your Firebase dashboard:

  1. Click Authentication
  2. Click Sign-in method
  3. Enable Email/Password, Google, and Anonymous

License

MIT.