Skip to main content

WCAG 2.2 accessibility engineer · Accessibility SME · Front-end developer · Boston, MA

I build software and systems where the only path is an accessible path

I'm Meredith Boyce, an accessibility and software engineer. For more than six years, I have tested web, mobile, and digital content against WCAG 2.2 AA, Section 508, ADA Title II, and WAI-ARIA standards, most recently as MassDOT's embedded accessibility subject-matter expert. Because I am blind, I am the end user, the auditor, and the remediator in one. Accessibility is my lived experience, not a checklist.

About

I am an engineer who has loved technology for as long as I can remember, and I specialize in making it accessible. I audit web and mobile products against WCAG 2.2, Section 508, ADA Title II, and WAI-ARIA (the dictionary explains each), and I write findings that tell a team exactly what to fix. Then I do what most auditors leave to others: I write the pull request, the tests that keep the fix in place, and the accessibility statement.

Most accessibility work arrives after the fact, and a year later the same findings return. I would rather build so that those findings cannot occur in the first place: the platform before ARIA, contrast failures treated as build errors, and tests for what a screen reader user notices first. Commons UI, this site, and the twelve common mistakes and remediations in the Lab put that approach into code.

When a project needs the full stack, I can write that as well. The two bots in the case studies run TypeScript on Node, with Firestore, scheduled jobs, and language-model integrations. Outside engineering, I coordinate programs and volunteers for The NDProj, a community for neurodivergent and chronically ill students in higher education.

Contact

I'm open to accessibility engineering, front-end, and design-systems roles, whether remote or in the Boston area. Email is the best way to reach me, but LinkedIn works as well.

Lab

Here are three small tools from this page's own source. Each is a plain custom element wrapped around native form controls, and the HTML works before any JavaScript loads.

Theme

All three themes come from a single token file, and every color pair is checked for contrast at build time. The site remembers your choice and applies it before the first paint on your next visit. Windows high-contrast mode is handled separately, with system colors.

Choose a theme

Typeface

Public Sans, the U.S. Web Design System typeface, is the default. Atkinson Hyperlegible Next, the Braille Institute's typeface, is drawn so that similar letters stay distinct, and it loads only when you choose it. The System option uses your device's own font and loads no web font at all. As with the theme, the site remembers your choice and applies it before the first paint on your next visit.

Choose a typeface

Contrast checker

This checker runs the same WCAG contrast calculation as the build, but live. Results appear as words, not just colors, and the summary is announced once you stop typing.

Tabs and a carousel

Tabs and carousels go wrong more often than they go right, so I built both to the WAI-ARIA Authoring Practices. The tabs below are the demo itself: the first holds the carousel, the second explains how it works, and the third shows the markup as authored. Without JavaScript, the tabs become a list of links above stacked sections, and the carousel becomes twelve stacked cards. Nothing rotates automatically.

Demo

1. Disabled while loading

A submit button set to disabled while the request runs vanishes from the tab order and goes silent. The user presses Enter, nothing happens, and nobody explains why.

Instead, set aria-busy="true" and aria-disabled="true", keep the button focusable, and announce the result when it returns.

2. Icon buttons with no name

A magnifying glass is not a label. A screen reader announces "button" and nothing else, and a voice-control user has no word to say.

Instead, give every icon-only control an accessible name, and hide the icon itself with aria-hidden.

3. Focus dropped on close

The dialog closes, and focus lands on <body>. For a screen reader user, that means a trip back to the top of the page.

Instead, return focus to the control that opened the dialog. The native <dialog> element does this automatically.

4. Color as the only signal

A red border signals an error, a green one signals success, and nothing else does. That is invisible to a screen reader and to many sighted people.

Instead, add text. Color can reinforce a state, but it can never carry one alone.

5. Live regions mounted with content

A status region created at the moment it has something to say is usually silent, because the screen reader never saw it change.

Instead, mount the region empty at startup and write to it later. Clear it first if you need to repeat the same message.

6. Placeholder as the label

Placeholders vanish on the first keystroke, most of them fail contrast, and a form full of typed values has no labels left at all.

Instead, always provide a visible <label>, and put hints in aria-describedby.

7. A div that clicks

A <div onClick> is invisible to the keyboard and has neither a role nor a name. Adding tabindex gets you halfway to a worse button.

Instead, use a <button> when it does something and an <a> when it goes somewhere.

8. The focus ring, removed

outline: none with nothing in its place means keyboard users cannot see where they are. Developers usually do this to hide the ring from mouse users.

Instead, style :focus-visible. Mouse users never see it, and keyboard users always do.

9. Skipped heading levels

An h2 is followed by an h4 because the h3 looked too big. Screen reader users navigate by heading level, so the outline now has a gap.

Instead, treat heading levels as structure and fix the size in CSS.

10. Errors nowhere near the field

The errors sit in a red list at the top of the form with no link to each field, and the field itself says nothing when a user reaches it.

Instead, connect the message with aria-describedby, mark the field aria-invalid, and link each summary item to its input.

11. Hover-only tooltips

If a tooltip appears only on hover, keyboard and touch users never see it, and it disappears the moment a magnifier user moves toward it.

Instead, show the tooltip on focus as well, let Escape dismiss it, and never put essential information inside it (WCAG 1.4.13).

12. Motion without an off switch

Autoplaying carousels, parallax effects, and animated counters ship with no pause control and no prefers-reduced-motion check.

Instead, let nothing move unless the user asks. This carousel does not rotate automatically, and its one fade lasts 0 ms under reduced motion.

How it's built

Tabs follow the APG tabs pattern with automatic activation. The whole tablist is one tab stop, the Left and Right arrow keys move between tabs and select them, and Home and End jump to the first and last tabs. Each panel is focusable, so the next Tab press lands on its content rather than skipping past it. Selection is shown by weight and a thick underline, not by color alone, and the URL hash opens a panel on load. Without JavaScript, the markup is a list of in-page links above stacked sections with headings; the script removes those headings because the tab itself now names the panel.

The carousel is the APG "tabbed carousel": the container is a group with aria-roledescription="carousel", the slide picker is a second tablist with one tab stop and arrow keys, and each slide is a tabpanel with aria-roledescription="slide" and an accessible name such as "3 of 12." Slides that are not current get the hidden attribute, so they leave the accessibility tree and the tab order together. Pressing Left or Right on the Previous or Next button also changes slides, so it does not matter which button has focus.

Where focus goes after Next, and why. Focus stays on the button, and a polite live region announces one short line, "Slide 3 of 12: Focus dropped on close," which gives the position and the heading but never the body. My first version wrapped the whole track in a live region, which read every paragraph on every press, but with JAWS, it was unbearable. Announce what changed, and then let the user decide whether to read it.

Carousel guidance is genuinely divided on this point. One school holds that focus should move to the new slide so that a screen reader user lands at the start of the new content. The other holds that forcing focus onto a non-interactive slide puts a focus ring on something that does nothing and interrupts a sighted keyboard user's path through the controls. These slides are not interactive, so the second argument wins here, and the live region addresses the first concern. A carousel whose slides hold links, buttons, and menus is a different problem: focus would need to move to the first element in each slide, and the carousel could not work as a "tab in, arrow within, tab out" widget at all. This component is deliberately scoped to slides you read.

Reading order is tab order. Controls sit above the track in the DOM and on screen; nothing is reordered with tabindex, so the page reads the same whether you arrow through it or tab through it.

What it refuses to do: rotate automatically. There is nothing to pause because nothing starts, and nothing is ever announced unless the visitor has pressed something. The dots are 24-pixel targets with a visible border. The selected dot is filled and larger, and in Windows high-contrast mode, it uses the system highlight color.

Both elements share a single small roving-tabindex helper with one set of tests. The Playwright suite tabs into each tablist, arrows through it, checks that hidden slides are unreachable, and confirms that pressing Tab on the Next button moves focus out of the carousel.

Markup

This is the HTML as authored. The script adds every role, and the page is complete without it.

<cui-tabs label="Carousel demo">
  <ul data-tabs>
    <li><a href="#tab-demo">Demo</a></li>
    <li><a href="#tab-how">How it's built</a></li>
  </ul>

  <section id="tab-demo">
    <h4 data-panel-heading>Demo</h4>
    <carousel-slider label="Twelve mistakes I keep finding">
      <div data-track>
        <section data-slide><h4>1. …</h4><p>…</p></section>
        <section data-slide><h4>2. …</h4><p>…</p></section>
      </div>
    </carousel-slider>
  </section>

  <section id="tab-how">
    <h4 data-panel-heading>How it's built</h4>
    …
  </section>
</cui-tabs>

Work

Filter the projects by the role you are filling. Public projects link to their repositories, and private ones link to their case studies.

  • Commons UI

    React 19TypeScriptViteVitestaxe-core

    Commons UI provides accessible-by-default React components for organizers, advocates, and mutual-aid networks. Its design tokens declare which colors sit on which backgrounds, and the build checks every pair with the WCAG contrast formula and refuses to emit CSS if any pair fails. The library includes an accordion, tabs, a dialog, a switch, form errors, live regions, and a menu button, and each component has keyboard tests and an axe scan. The menu button also ships as a framework-free custom element, which powers both menus on this site.

  • MedConnect

    TypeScriptSlack BoltFirestoreGeminiGoogle Calendar

    MedConnect is a Slack app that turns appointment screenshots and one-line messages into structured records, calendar events, and timed reminders for a household coordinating medical care across many clinicians. The repository is private.

  • PlannerBot

    TypeScriptdiscord.jsFirestoreGemininode-cron

    PlannerBot is a Discord bot that turns a nonprofit's server into its planning system: to-dos with subtasks and private assignment threads, projects, events scheduled across four calendars, and a social media pipeline that reads its own Instagram metrics and treats alt text as a hard requirement. It contains 7,749 lines of TypeScript across 78 modules, and its repository is private.

  • The NDProj brand system

    HTMLCSSHeadless Chrome

    I wrote the logo, LinkedIn covers, and post templates for a nonprofit in HTML and CSS, and headless Chrome renders them to PNG, so every asset can be reproduced from its source. The palette is sampled from the logo and documented with its contrast ratios, including the colors that are never allowed for text.

  • This site

    HTMLCSSTypeScriptWeb ComponentsPlaywright

    This site uses no framework. It is built from static HTML, cascade layers, container queries, and seven small custom elements of its own. Several parts come directly from Commons UI, installed from that repository as framework-free custom elements with their stylesheets: both menu buttons, the Lab's tabs, the contrast checker's fields, the live region, the buttons, and the skip link. The Commons UI token pipeline also runs here, ported to TypeScript. On every deploy, axe-core audits the site in a real browser.

Experience

Here is my CV in brief. The full résumé is a two-page tagged PDF.

  1. to

    Digital Accessibility Engineer and Accessibility SME, MassDOT (contract via LanceSoft)

    Embedded with MassDOT Digital Services as the accessibility subject-matter expert and QA partner for cross-functional web and mobile teams.

    • Documented more than 400 defects over nine months, each with its severity, user impact, WCAG success criteria, reproduction steps, expected behavior, and remediation recommendation; verified fixes with engineering through to closure.
    • Validated products against WCAG 2.1 and 2.2 AA, Section 508, and ADA Title II with JAWS, NVDA, VoiceOver, keyboard-only navigation, browser inspection, and automated scans.
    • Reviewed third-party VPATs and ACRs for product risk and compliance posture; translated findings into recommendations for technical and nontechnical stakeholders.
    • Worked with designers, engineers, QA, product, and program teams to build accessibility acceptance criteria into delivery so that defects were caught before release.
    • Produced accessible documentation and guidance for compliance planning and cross-agency communication.
  2. to

    AmeriCorps Service Member, Perkins School for the Blind

    Completed more than 1,000 hours of national service in academic programming for students with visual impairments.

    • Acted in tandem with educational staff to handle day-to-day school functions.
    • Taught braille and supported students in an academic setting.
    • Supervised and coordinated volunteers for events, handling day-of direction, delegation, and follow-up.
    • Planned, staffed, and ran events and service activities, managing logistics, participant needs, accessibility requirements, and timelines.
    • Coordinated daily programming and communication among educators, staff, students, volunteers, and service partners.
    • Represented AmeriCorps at the Massachusetts State House and in meetings with Senators Warren and Markey on AmeriCorps funding; represented the educational programming division at the 2024 AmeriCorps Summit.
  3. to , and to

    Founder and Accessibility Engineer, Boyce Accessible Technologies

    Founded and ran an accessibility consulting practice, first for small-business websites and mobile applications and later for enterprise engagements, owning every stage from intake and scoping through testing, remediation, training, and delivery.

    • Built a company's internal and external knowledge-base design system, using HTML, CSS, XML, ARIA, jQuery, and Froala components, for delivery to a Fortune 100 client.
    • Created the shared component library and design-token approach that I later brought to MassDOT.
    • Tested and remediated accessibility issues in Swift, Kotlin, and JavaScript applications against WCAG 2.1 AA, with a focus on assistive-technology compatibility.
    • Advised client teams on accessible design patterns, keyboard interaction, semantic structure, ARIA usage, defect remediation, and inclusive QA practices.
    • Delivered client-facing recommendations, training guidance, and implementation documentation.
    • Managed scope, priorities, timelines, and changing requirements directly with clients.
  4. to

    Accessibility Subject Matter Expert, Equivalent.Design

    • Developed and tested accessible digital materials for iOS and Android, covering screen reader behavior, focus order, labels, gestures, mobile accessibility patterns, and accessible SVG.
    • Created methodologies for outsourcing testing to third parties and for automated testing from JSON scripts.
    • Shipped code, including the optimized core algorithm behind a key function, in three major releases of a back-end product that went live and was later acquired by a larger firm.
    • Collaborated with technical and nontechnical stakeholders to clarify requirements, prioritize issues, and communicate accessibility risk and solutions.
  5. to

    Accessibility Consultant, Industry Lead, Level Access

    Served as the coordinating point between client product, engineering, QA, design, and leadership teams across as many as 100 enterprise clients.

    • Managed roughly twenty-three concurrent client workstreams with competing deadlines, owning priorities, issue tracking, and cross-team follow-up.
    • Performed accessibility testing and audit review with screen readers and automated tools; documented findings against WCAG 2.1 AA with user impact and remediation guidance.
    • Audited and wrote enterprise code in HTML, CSS, JavaScript, React, Angular, and Vue.
    • Delivered accessibility training, consultation, and written guidance to 100 client teams.
    • Built accessibility programs from the ground up for multiple clients, including planning, roadmap input, progress reporting to leadership, and ongoing review cycles.
    • Served as secretary of the Women @ Level employee resource group.
  6. to

    Partner, Diversity Crew

    • Served as the partnership's accessibility and disability subject-matter expert while also running my consulting practice.
  7. to

    Technical Lead, eSSENTIAL Accessibility

    • Supported consumer-facing mobile applications as a U.S.-based contractor, mostly in Java with some Swift and Kotlin, and provided mobile development when extant applications were discontinued.
    • Created and audited reports on mobile application and website code against WCAG 2.0 and 2.1 for high-profile clients.
    • Managed a diverse team of developers and code reviewers.
  8. to

    Implementation Intern, AudioEye

    • Wrote JavaScript and WAI-ARIA markup for client websites and enterprise software.
    • Audited the Digital Accessibility Platform (Java and PHP) against WCAG 2.1 and Section 508; suggested and implemented improvements, including making the platform itself usable by blind users.
    • Tested mobile and web applications in Java and Swift and produced technical reports for Fortune 500 clients.
  9. to

    Advisory IT Specialist, South Carolina School for the Deaf and the Blind

    • Learned JAWS and refreshable braille displays as a student, then taught accessible technology to peers and, later, to students.
    • Led accessible-technology workshops and office hours over several years.
    • Worked with small groups to develop a basic Android application.

Honors

  • 100 Changemakers, AI Powered Women Conference at MIT2026
  • UN Women #SheInnovates Honoree2019
  • White House Champions of Change2015

Leadership

  • Volunteer program and project coordinator, The NDProjCurrent
  • AmeriCorps representative to the Massachusetts State House2024 to 2025
  • CSforALL Accessibility Initiative Advisory CommitteeInaugural member, since 2018

Education

  • Converse UniversityComputer Science coursework, B.S. program, three years; French minor completed

Skills

Standards and auditing
WCAG 2.0, 2.1, and 2.2 at A, AA, and AAA; Section 508; ADA Title II; WAI-ARIA and the Authoring Practices Guide. Manual and automated audits; keyboard, screen reader, and ARIA validation; VPAT and ACR review for product risk; test-case development and regression testing; inclusive design.
Remediation and documentation
Defects documented with severity, user impact, success criteria, reproduction steps, expected behavior, and a recommended fix, then verified with engineering through to closure (400+ at MassDOT); remediation guidance and the pull requests that carry it out; acceptance criteria built into delivery so that defects are caught before release; accessibility statements; guidance for technical and nontechnical readers.
Assistive technology
JAWS (daily), NVDA, VoiceOver, and TalkBack, used with a Mantis Q40 braille display across web, iOS, and Android. I test as the very user the standard describes.
Mobile accessibility
iOS and Android testing and remediation: native apps in Swift, Objective-C, Kotlin, and Java, as well as React Native apps; VoiceOver and TalkBack behavior, focus order, and custom actions.
Testing tools and QA
axe DevTools and axe-core, Accessibility Insights, Lighthouse, browser developer tools; Playwright in Chromium and WebKit, Vitest, Testing Library; keyboard-walk, reflow, contrast, and focus-order tests that run in CI; Jira, test documentation, defect tracking.
Languages
HTML, CSS, SVG, JavaScript, TypeScript (strict, everywhere), Python, SQL; Kotlin, Swift, Objective-C, C, and Java for mobile work.
Front end
React and React Native, Angular, Vue; web components, design tokens with contrast enforced at build time, cascade layers, container queries, forced-colors and reduced-motion support; Vite.
Back end and integrations
Node, Slack Bolt, discord.js, Firestore, the Google Calendar API, the Meta Graph API, and the Gemini REST API for parsing, drafting, and vision; node-cron, Docker, and Railway; GitHub Actions and GitHub Pages.
Ways of working
Agile (Scrum) with QA, engineering, product, and design; plain-language documentation and field guides for non-engineers; release notes that ship with the code; small, reviewable pull requests; Claude Code as a pair programmer (as of late September 2026), while the specification, the live testing, and the acceptance of every change remain my responsibility.
Communication and judgment
Findings translated for engineers, designers, product owners, and executives, each in the terms that matter to them; client-facing recommendations, training, and implementation guidance; scope, priorities, timelines, and changing requirements managed directly with clients; roughly twenty-three concurrent workstreams with competing deadlines at Level Access; accessibility programs built from the ground up, with roadmap input and progress reporting to leadership; and a daily screen reader user's judgment about what matters first.
Leadership, teaching, and advocacy
Served as the coordinating point between client product, engineering, QA, design, and leadership teams for as many as 100 enterprise clients; managed a team of developers and code reviewers; spent 1,000+ hours teaching braille and supporting students at Perkins School for the Blind through AmeriCorps; supervised hundreds of volunteers and ran at least six events from start to finish; represented AmeriCorps at the Massachusetts State House; coordinate programs and volunteers for The NDProj; assistant scoutmaster for a pilot-program coed Scouts BSA troop from 2020 to 2022; employee resource group secretary at Level Access; White House Champion of Change in 2015 as a "Young Woman Empowering Her Community."
Spoken and written languages
English and braille (native); French and American Sign Language (professional working proficiency).

My approach

These five rules govern every project shown here, as well as the source code of this page.

  1. Accessibility is a build error, not a review comment. The tokens on this site declare which colors sit on which backgrounds. The build checks every pair and refuses to emit CSS if any pair fails. While I was building this page, the check caught me requiring a focus ring to contrast with both the page and a dark button. The button is not the ring's adjacent color, and the build flagged the mistake before any reviewer saw it.
  2. Use the platform before you use ARIA. Use real buttons, real labels, native radio groups, and the native <dialog>. Reach for ARIA only where HTML has no answer: aria-pressed on a filter toggle, aria-current on the section you are reading.
  3. Focus goes where the user's attention should go. Skip links land on a focusable main element. In-page links move focus to their section. Error summaries take focus. Nothing is disabled while it is working, because disabled controls vanish from the tab order and go silent.
  4. Never rely on color alone. Every state has a cue beyond color: the words "Pass" and "Fail," a check mark on the pressed filter, and an underline on the current section. Icons are hidden from assistive technology and sit next to a visible label.
  5. Test the behavior, not just the markup. The axe-core engine catches missing names and bad roles, but it cannot tell you that focus went nowhere on submit. On every deploy, the test suite runs axe in a real browser, tabs through the page, resizes it to 320 pixels wide, and checks that nothing scrolls sideways, ever.

Case studies

These are two production TypeScript applications whose source I cannot publish. Here is how they are built and how they run.

Slack app · Node · Firestore · Gemini

MedConnect: appointments from screenshots to reminders

Coordinating care across many clinicians means appointments arrive as portal screenshots, confirmation emails, and half-remembered phone calls. MedConnect lives in the household's Slack and turns each of those into a structured record, a calendar event, and reminders for the patient and a support person.

MedConnect architecture Slack sends slash commands, button clicks, and mentions to a Slack Bolt app over HTTP. The app calls services that book appointments, match doctors, and build calendar files. Services call Gemini to classify and extract data from text and images, write records to Firestore, and create Google Calendar events. Cron jobs read Firestore and send reminder direct messages back to Slack. Slack workspace commands · mentions Slack Bolt app (HTTP) commands · interactions events (text or image) Services book · match doctor calendar · ics Gemini (REST) classify · extract JSON Firestore appointments · doctors Google Calendar events Cron jobs reminders · follow-ups MedConnect architecture Slack sends slash commands, button clicks, and mentions to a Slack Bolt app over HTTP. The app calls services that book appointments, match doctors, and build calendar files. Services call Gemini to classify and extract data from text and images, create Google Calendar events, and write records to Firestore. Cron jobs read Firestore and send reminder direct messages back to Slack. Slack workspace commands · mentions Slack Bolt app (HTTP) commands · interactions events (text or image) Services book · match doctor calendar · ics Gemini (REST) classify · extract Google Calendar events Firestore appointments · doctors Cron jobs reminders · follow-ups
Slack calls one Bolt app over HTTP. Feature modules register their own commands, interactions, and events. Shared services book appointments, and cron jobs read Firestore to send reminders back as direct messages. Dashed lines mark scheduled work rather than user-triggered work.

How a screenshot becomes an appointment

  1. Someone @mentions the bot with an image. The handler checks the sender against the owner and an allowlist of trusted uploaders set in the environment. Anyone else receives a polite refusal message, and the bot never downloads the image.
  2. One channel receives several kinds of screenshots (appointments, referrals, and lab orders), so the image is classified before it is routed: a cheap Gemini vision call returns a type, and each type's handler either claims the mention or lets it pass through.
  3. The claiming handler asks Gemini for a typed JSON shape (title, date, times, category, doctor hint, address). Prompts live next to the type they produce; unparseable output is an error, not a guess.
  4. The doctor hint is matched against the directory with a fuzzy matcher: substring or Levenshtein distance for whole names, then per-token matching that ignores titles such as "Dr.," which would otherwise match every doctor at once. No match means the bot asks, with a one-click "add this doctor" flow, rather than booking with the wrong clinician.
  5. A single bookAppointment service creates the Google Calendar event, writes the Firestore record with the event ID, and formats the Slack card. The slash command and the mention path both call it, so booking can go wrong in only one place.

Scheduled jobs

  • 40-minute reminders run every five minutes and pick up appointments in a 35-to-45-minute window. Each record stores reminderSentAt, so the job is idempotent across restarts and never sends a duplicate message.
  • Next-day follow-up at 11:00 a.m. Eastern (America/New_York) asks how the previous day's visit went and opens the record for visit notes and next steps.
  • Referral and lab-order reminders nudge until the referral is resolved, and support-person reminders go to the second person on an appointment.

Structure

Entry points
Slash commands /appointment, /doctor, /directory, /find; @mentions with text or an image; button and modal interactions.
Layout
commands/, interactions/, events/, jobs/, services/, db/, types/, utils/. Each feature exports a register(app) function; index.ts is only wiring.
Data
Typed Firestore documents: Appointment, DoctorCard, Referral, TestReferral. Optional fields are documented at the type, including which Slack ID is set by which UI control.
Integrations
Gemini over plain fetch (no SDK: one small function per prompt shape, typed return). Google Calendar through a service account. A hand-written RFC 5545 .ics generator with a VTIMEZONE block, so a support person without calendar access can still import the event.
Runtime
Node in a Docker image on Railway, with restart on failure. Unhandled rejections and exceptions are logged with a per-module prefix, so production logs can be grepped by feature.

Decisions I'd defend

  • Classify before parsing, because the channel is not a reliable signal of content.
  • Never invent a doctor. Ambiguity becomes a question with a button, not a best guess.
  • Idempotency lives on the record, not in process memory, so restarts are safe.
  • Slow work is visible work: the bot replies "Reading that referral…" immediately, then posts the result, so nobody wonders whether the mention landed.

Discord bot · Node · Firestore · Gemini · Google Calendar · Meta Graph API

PlannerBot: a nonprofit's planning system, inside its Discord

The NDProj is run by two volunteers, and its plans used to live in chat scrollback. PlannerBot turns the Discord server itself into the planning system: you speak to it in sentences, it keeps the record, and the record comes back as a card with buttons wherever the conversation happens. I held it to the same rules that I apply to a product in an audit. Nothing goes out without alt text. Every action confirms in one message, not three. Nothing lives where the wrong person could reply to it. Nothing is deleted unless its deliverables survive.

PlannerBot architecture Discord sends slash commands, component interactions, and mentions to a discord.js client. A registry dispatches buttons, selects, and modals by custom ID prefix. Services render cards, run readiness checks, and schedule events. They call Gemini to extract fields and draft captions, Google Calendar to check conflicts, and Firestore to store projects, to-dos, events, posts, and settings. Cron jobs post digests and reminders to Discord. An instance lock document in Firestore makes an old deployment stand down when a new one starts. Discord server commands · buttons mentions discord.js client one command per file registry by customId prefix @mention intent routing isActive() at every entry Services cards · threads readiness · cadence scheduling · metrics Gemini (REST) extract · draft · ideas Firestore projects · todos · events posts · settings Google Calendar conflict checks Cron jobs (9:00) digest · cadence · metrics Instance lock settings/instance:active PlannerBot architecture Discord sends slash commands, component interactions, and mentions to a discord.js client. A registry dispatches buttons, selects, and modals by custom ID prefix. Services render cards, run readiness checks, and schedule events. They call Gemini to extract fields and draft captions, Google Calendar to check conflicts, and Firestore to store projects, to-dos, events, posts, and settings. Cron jobs post digests and reminders to Discord. An instance lock document in Firestore makes an old deployment stand down when a new one starts. Discord server commands · buttons · mentions discord.js client one command per file registry by customId prefix @mention intent routing isActive() at every entry Services cards · threads · readiness cadence · scheduling · metrics Gemini (REST) extract · draft · ideas Google Calendar conflict checks Firestore projects · todos · events posts · settings Cron jobs (9:00) digest · cadence · metrics Instance lock settings/instance:active
PlannerBot has the same shape as MedConnect, with discord.js in place of Bolt, plus an instance lock: each deployment claims a Firestore document on start, and the previous deployment goes quiet when the claim changes. Dashed lines mark scheduled or infrastructure work.

To-dos, subtasks, and private threads

A to-do starts as a sentence. "@PlannerBot review the treasurer résumé Ila emailed me, @Ila to handle" becomes a record with a title, the details, who thought of it, who owns it, and a due date read from "by Friday." A slash form is the alternative for anyone who prefers one.

  1. Assigned work is invisible to everyone else. Assignment opens a private thread for the assignee and the assigner, posts the card there, and removes the public one. Only the originator or the owner can close a to-do; being the assignee does not let you reassign it.
  2. Subtasks go one level deep, on purpose. A subtask is a full to-do with its own thread, card, notes, and files, opened by a mention in the parent's thread, a button on the card, or the form. A subtask cannot have subtasks; a request for one is refused with a pointer to the parent. The parent's card shows progress: "Subtasks (1/3 done)".
  3. Closing a parent settles everything under it. Open subtasks close with credit to their assignees, their notes and files roll up onto the parent, a full Deliverables message is left in the thread, and the thread is locked and archived. Reopening restores it.
  4. Deliverables are reachable from the interface, not a command. Every card has a Deliverables button that lists each file as a permanent jump link, and each person involved has a private Deliverables thread that receives the list when a to-do closes.
  5. Names resolve to people. Matching ignores case, spaces, and punctuation, so "Ila," "ila :)," and a username all resolve to the same person, and forms that cannot carry an @mention still assign the right one.

Cards are sticky: while a to-do is open, every note, reply, or edit brings its card back to the bottom of the thread, and duplicates in one channel collapse into one. Any private thread whose item is deleted or reassigned is removed, so nobody replies into a dead thread.

The post pipeline

One record follows each post through its whole life: Idea → Drafted → In review → Approved → Posted. Planned posts are the calendar, posted ones are the log, and the same document is both. Capture works by intent, not only by prefix: "create a post about…" files a post, and a bare "on LinkedIn" does not, because plenty of to-dos are about the platforms themselves.

  1. Readiness is checked, not assumed. "Request review" runs a pre-flight checklist. A missing caption or a leftover [placeholder] blocks submission; missing hashtags, a missing graphic, alt text, pillar, or date produce warnings instead. Approve and Request Changes notify the owner in a private thread, and a caption edited after approval goes back to review.
  2. Alt text is required to count. A post counts as "written with graphics" only when it has both an image and alt text, and the writing-week coach says exactly which is missing. The Graphic button nudges the writer when the alt text reads like a caption.
  3. Drafting is assisted, never automatic. Gemini writes in the organization's documented voice, with Shorter, Warmer, More direct, and Another go options; nothing is saved until someone chooses a version, and facts that were not supplied come back as placeholders, never invented. Content pillars, a Saturday balance nudge, and five ideas that lean toward whichever pillars are running dry keep the calendar honest.
  4. Metrics come from the platform. Pressing Metrics on a posted Instagram card reads likes, comments, shares, saves, and reach through Meta's Graph API from the post's link and offers Save or Edit first; nothing is written until you choose. A nightly job re-reads them 3, 7, and 30 days after posting and never overwrites a newer manual entry. LinkedIn exposes no such API, so its metrics are entered by hand, but the form warns of this.
  5. Copy works on a phone. "Copy caption" sends the caption, hashtags, and alt text as three separate plain messages, because Discord mobile copies a whole message at a time.

Events across four calendars

"Create event: body doubling this Saturday 2–4pm in Quiet Study 1, I'll host" lands as a forum post and on the organization's Google Calendar, and edits sync both ways. /event find ranks free slots across four shared calendars using only busy intervals, never titles, with weekday evenings and weekends first, a fifteen-minute buffer, and at most two picks a day. Creating or moving an event onto someone's busy time triggers a warning with Move-to buttons.

Structure

Commands
Seven slash-command groups, one per file (/project, /todo, /post, /event, /stats, and two more), listed once in commands/index.ts. Twenty-five services do the work behind them.
Interactions
Buttons, selects, and modals are keyed by customId prefix (prefix:arg1:arg2). A registry holds three handler maps, each feature module registers its handlers in them, and one dispatcher splits the ID and calls the handler with its arguments.
Jobs
Eight scheduled jobs on node-cron: the weekly digest, post reminders, the writing-week coach, the pillar balance nudge, the metrics re-read, the month-end report, the Monday productivity summary, and release notes to the docs channel.
Record keeping
An activity log that outlives the item: assignments, completions with on-time status, hosted events, and posts are written as rows with deterministic IDs, so deleting a to-do keeps the credit and reopening one removes it. /stats reports per member and per team, with a CSV export.
Documentation
A README for engineers and a field guide for the team: one WCAG 2.2 AA HTML page, served by the bot at a stable URL and pinned in Discord, with axe-core run on every change and zero violations allowed. Release notes ship with the code: a Markdown note in the repository is posted automatically when the deploy that carries it comes up, and the guide's version is derived from the count of notes, so nobody bumps a number by hand.

Three production problems, solved

Overlapping deploys. Railway starts the new container before stopping the old one, and for those few seconds, Discord delivers every message and cron tick to both. Once, a subtask got two threads, and a delete cascade ran only halfway. The fix is an instance lock: on start, each instance writes its ID to a settings document and subscribes to it, and the moment a newer instance claims it, the older one ignores messages, interactions, and cron until it is stopped. The lock is per bot user, so a dev bot beside prod never silences prod.

A client that died and reported ready. A Discord gateway outage showed that a destroyed client could still fire its ready event. Login now retries with exponential backoff and a fresh client on every attempt.

A glyph that rejected every card. Discord validates message components on its servers, and a checkbox character that is not a Unicode emoji once rejected every project card with an open task. A card probe sends every card variant to Discord and deletes it, so that class of failure is caught before a deploy.

By the numbers

  • 78 modules and 7,749 lines of TypeScript; 5 runtime dependencies.
  • 7 slash-command groups, 8 scheduled jobs, 25 services, 24 release notes.
  • Zero axe-core violations on the field guide across 36 rules, in both themes.
  • Four Google calendars in the scheduler, one Meta Graph API integration, one Gemini integration for parsing, drafting, and ideation.

I designed the data model, the interaction registry, the permission rules, and the readiness checks, and I set the accessibility bar the bot enforces on everyone else's posts. I tested every feature live in the server before it shipped, usually in the same hour, and the production problems above were mine to diagnose from the logs.

Writing

These are notes on accessibility engineering: what audits keep finding, how I fix each problem, and how I maintain that fix. The three newest posts appear here. The archive holds all of them, and the Atom feed carries the full text.

  1. TalkOver, revisited

    · 3-minute read

    Years ago, I wanted to rewrite TalkBack to behave like VoiceOver. I finally read the TalkBack source. Most of the "rewrite" already ships as a settings screen.

  2. Accessibility is a build error, not a review comment

    · 2-minute read

    I made this site refuse to compile when a color pair fails WCAG contrast. Here is why, and what it caught on day one.

Dictionary

The standards I audit against, in plain terms. If you hire for this work without living in it, this is the section for you.

WCAG 2.2

The Web Content Accessibility Guidelines, published by the W3C, the body that maintains the web's standards. Version 2.2 is current. It is a list of 86 testable rules, called success criteria, under four principles: content must be perceivable, operable, understandable, and robust. Every rule has a level. Level A rules remove the barriers that shut people out entirely. Level AA rules make a product usable in practice, and this is the level laws and contracts cite. Level AAA rules go further than most products can.

When a posting says "WCAG 2.2 AA," it means every Level A and Level AA rule: 55 in all. An audit checks each one and reports which pass, which fail, and which do not apply. The W3C's How to Meet WCAG 2.2 lists every rule with its techniques, and the accessibility statement for this site shows what a rule-by-rule report looks like.

Section 508

A United States federal law, part of the Rehabilitation Act, that requires federal agencies to build and buy technology that people with disabilities can use. Since 2018 its technical standard has been WCAG 2.0 at Level AA, with added rules for hardware, software, and documents. Meeting WCAG covers most of it. Vendors who sell to the government document their conformance in an ACR, defined below.

ADA Title II

The Americans with Disabilities Act. Title II covers state and local governments. In April 2024 the Department of Justice named WCAG 2.1 at Level AA as the standard for their websites and mobile apps, with a deadline of April 2026 for larger governments, including state agencies, and April 2027 for smaller ones. MassDOT was working to meet that deadline while I was its embedded accessibility SME. Title III covers businesses open to the public, and courts have applied it to their websites.

VPAT and ACR

A Voluntary Product Accessibility Template is a blank form; a completed one is an Accessibility Conformance Report. It states, rule by rule, whether a product supports, partially supports, or does not support each criterion, with remarks. Buyers, especially governments, ask for one before they sign. I read vendors' ACRs for what they claim, and then I test whether the claim holds.

WAI-ARIA and the Authoring Practices

ARIA is a set of HTML attributes that tell assistive technology what a custom control is and what state it is in: this element is a tab, and it is selected. It is a last resort, because native HTML elements already carry that information. The Authoring Practices Guide (APG) describes the expected keyboard behavior for common patterns such as tabs, menus, and carousels. The tabs and the carousel in the Lab follow it.

Contrast ratio

A number from 1 to 21 that measures how far apart two colors are in brightness. WCAG at Level AA requires 4.5:1 for ordinary text and 3:1 for large text, control boundaries, and focus indicators. The build for this site computes every declared color pair and refuses to ship if one falls short. The Lab has a live checker.

Assistive technology

The software and hardware people use to operate a computer differently. A screen reader (JAWS, NVDA, VoiceOver, TalkBack) speaks the screen or sends it to a braille display. A keyboard, a switch, or voice control replaces the mouse. Magnification and high-contrast modes change how the screen is drawn. I test with JAWS and a braille display every day, because I use them every day.

Audit and remediation

An audit checks a product against every applicable rule, by hand and with tools, using assistive technology. Automated scanners find roughly a third of the problems; the rest take a person. The result is a report: each defect with its criterion, severity, who it affects, how to reproduce it, and how to fix it. Remediation is the fixing. An accessibility statement then tells users what they can expect, and this site has one.