Built 2021 · Status Retired · Stack React 16, Material-UI 4, Strapi 3 on Node 14, PostgreSQL, Express, Slack API
EZ Message Manager scheduled recurring emails and Slack messages to employees, where the recipient list was a stored query that ran at send time rather than a spreadsheet somebody uploaded. It got as far as a working first version, and I published all three repositories as open source.
Purpose
I needed to send recurring onboarding messages to new employees, and the tools I had all assumed a static contact list. A list uploaded on Monday is wrong by Friday, because new people were hired and others moved teams. I wanted to describe an audience once, in terms of facts about employees, and have the system work out who was in it on the morning each message went out.
The second purpose was mine rather than the product's. I had been teaching myself React and Node.js in my spare time, had become interested in the headless CMS pattern, and wanted a real project to learn on. I brought in a freelance developer to move faster, though we were both new to the stack, and the code shows it. That is precisely why I open sourced it rather than quietly retiring it.
What it did
It ran as three separately deployed pieces. The backend was Strapi on Node.js over a PostgreSQL database, exposing both a REST API and a GraphQL endpoint, with Strapi's built-in JWT authentication handling users and accounts. It deployed to Heroku straight from GitHub, so a push rebuilt it. The frontend was React with Material-UI on the Devias admin template, talking to GraphQL through React Apollo, deployed on Netlify with the same automatic build from GitHub. The scheduler was a third service running under PM2, the Node.js process manager, with a Procfile, an ecosystem.config.js, and a modified start script so it could run as its own Heroku app. Email templates were built in Unlayer, a free drag and drop editor, and could interpolate employee profile and metadata fields. SMTP pointed at Mailtrap for testing.
The interesting part was the contact lists. Employees carried tags and arbitrary metadata fields, things like organization, manager, demographic, and area of expertise. A contact list was not a set of rows; it was a GraphQL query stored in the database and executed each time a scheduled message fired, so the audience was recomputed at send time. A query selecting employees hired between two dates and carrying a particular tag returned whoever satisfied that description that morning.
That design ran into a real limitation. GraphQL has no concept of "now", so a query cannot express "hired in the last 90 days" on its own. My workaround was to preprocess the stored query with regular expressions before executing it, substituting the current date wherever the query said today, and applying an offset in angle brackets, so a trailing <-90> shifted the substituted date back 90 days. It worked, and it is the kind of thing you write when you are learning. Rewriting it now, I would use Strapi's built-in REST API filters and let the framework express the date arithmetic.
Features
- Employee management assigned tags and custom metadata fields to each employee record, covering things like organization, manager, and expertise.
- Dynamic contact lists were stored as GraphQL queries and re-executed on every send, so the recipient list reflected the current state of the employee data.
- Email templates were composed in Unlayer and could merge in employee profile and metadata fields.
- Email scheduling paired a template with a contact list and a time.
- Slack scheduling did the same thing for Slack messages.
- JWT authentication for users and accounts came from Strapi's built-in provider rather than anything hand rolled.
Impact
This one is honest about what it was: a learning project that solved a problem I personally had, shared publicly so other people could read it, fork it, or improve it. I do not have adoption numbers to report and would not invent any. Its useful legacy is the write-up itself, particularly the dynamic contact list approach and the account of where that approach broke down, which is the part I would tell someone about before they built the same thing.
It cannot be run as documented any more. The deployment described here depends on Heroku's free dynos, which were discontinued in 2022, and it is built on Strapi 3, which is no longer supported.
Screens



