Skip to main content
Web Wizard Dev
All projects

Client project · Labor union

Teachers' Union Website & Member Platform

Moved a 14,000-member teachers' union off a hard-to-edit Drupal site onto one platform where staff publish content, keep the roster current and target members by school or district, without a developer.

Client
Philadelphia Federation of Teachers
Industry
Labor union
Timeline
May 2026 to present
Year
2026
Platforms
Web
PFT homepage with the breaking-news banner, a hero carousel and quick links to the contract, FAQs, enrollment and Find Your Rep

Results at a glance

14,000+members on the roster the platform serves
260archived posts from 2008-2026 recovered with their original dates
92-96 to 100Lighthouse accessibility on all 7 key public pages
75%less time to count an email audience as filters change (805 to 203 ms)

The challenge

Baseline · email audience count805 msto update the member count on each filter change

  1. Much of the old site was frozen in code: the staff-rep lookup was a script with the school-to-rep mapping typed into arrays, the membership figure had drifted about 360 members above the real roster, and endorsements were a hand-edited list. Every change needed a developer.
  2. The roster is a monthly CSV from an outside vendor that replaces the member list each time. Between two exports the vendor changed its member IDs (0 of 13,700 matched) and 41 members changed email address, so a naive import would have duplicated or wrongly removed thousands of people.
  3. Over 14,000 members needed to sign in without anyone creating accounts. Passwords add support load, and one-time links sent to school email are often "clicked" by the mail scanner before the teacher sees them.
  4. Staff wanted to email members by school or legislative district, honor every unsubscribe, and know exactly who did not get a message. A full-roster send runs for hours, far past a serverless function's time limit, and the audience count took 805 ms to update on each filter change.
  5. Staff need different permissions over sensitive member data, including bulk exports, and the union needs a record of who looked at or exported what.
  6. 18 years of press releases lived on the old site, many stamped with a later migration date, with links pointing at pages and tracking services that no longer exist.

About the project

Designed and built the new pft.org for the Philadelphia Federation of Teachers, replacing a Drupal site hosted by the national union. The public site covers news, events, press releases, the contract, enrollment, a staff-rep lookup and election endorsements, and staff edit all of it from an admin area. Behind it sits a member platform: passwordless sign-in for everyone on the roster, a monthly roster import with preview and safe deactivation, an email campaign tool that targets members by school or legislative district, role-based staff access, and an audit log of every action on member data. 18 years of archived press releases were recovered with their original dates, and every old URL still works.

Scope

  • Site migration
  • Member portal
  • Passwordless sign-in
  • Roster import
  • Email campaigns
  • Admin tools
  • Content archive recovery
  • Accessibility & SEO

What it does

  • A public site staff edit themselves

    News, events, press releases and PD posts with a rich-text editor and a shared media library. The homepage carousel and breaking-news banner are editable. Site-wide search runs on a static index built at deploy time.

  • Find Your Rep

    293 school listings mapped to their staff reps, in the database, with an admin screen to add schools and reassign reps.

  • PFT Votes

    Endorsed candidates by office and district, plus canvass and phone-bank shifts. Candidate labels are derived from the seat, so the card and the district filter can never disagree.

  • Passwordless member sign-in

    A member types their email address and gets a 6-digit code, with a sign-in link as a backup. The roster decides who may sign in, so no one creates accounts. Access is re-checked on every request, and a member who leaves the roster sees an "access ended" screen.

  • Roster import

    Staff upload the vendor's monthly CSV. The import checks the file before touching anything, shows a preview (new, returning and leaving members), and marks departed members inactive rather than deleting them. A drop of more than 20% needs an admin to sign off.

  • Email campaigns

    Templates, drafts, a live preview and merge fields. Audiences are built by filters: school, job title, bargaining unit, staff rep, dues type, years of membership, and 5 legislative-district levels (U.S. House, state senate, state house, county, city council). The member count updates as filters change, in 203 ms instead of 805 ms after a rewrite of the counting query. Sends go through a queue that records every recipient, retries failures, and produces a failure report and CSV. Unsubscribe links work without signing in, and an unsubscribe always overrides being added to an audience by hand.

  • Staff roles and an audit log

    Viewer, editor, admin and developer tiers, stored apart from the member roster so a roster import can never demote an admin. Changes to staff accounts, email campaigns, audience exports and every page of a member search are written to an audit log that admins can filter and read.

  • The old archive is back, and old links still work

    260 press releases and PD posts from 2008 to 2026, with their 337 images and PDFs, imported as ordinary posts with their real publication dates. Every old URL has its own permanent redirect.

Our approach

  1. Moved every frozen piece into the database with an admin screen: 293 school listings and their reps, candidates and volunteer shifts, homepage slides and banner, and a shared media library. The membership figure is computed from the live roster at build time, so it cannot drift again.

  2. Keyed the roster on the vendor's own stable ID and made the import validate before it writes: it stops on missing or duplicate IDs, detects shifted columns, previews every change, and marks departed members inactive instead of deleting them. A drop of more than 20% needs an admin to confirm.

  3. Built passwordless sign-in around a 6-digit code, which a mail scanner cannot use up, with a sign-in link as a backup. The roster decides who may sign in, and access is re-checked on every request.

  4. Wrote one SQL function that defines an audience, so the preview count and the send list can never disagree, and restructured it around a shared base set, cutting the count from 805 ms to 203 ms. Sends go through a database queue drained by a scheduled job in small batches. Every recipient gets a record, failures retry and end up in a downloadable report, and an unsubscribe always wins over a manual include.

  5. Kept staff roles in their own table, separate from the roster, and enforced them twice: in the app and in database row-level security. Changes to staff accounts, email campaigns, audience exports and every page of a member search are audit-logged. 2 security audits reviewed the auth, data and admin surfaces, and a SQL test suite checks the live database's permissions on every merge to main.

  6. Recovered the archive with a one-off import that took each post's date from the dateline printed in its body where the stored date was a migration artifact. It checked every link: 47 dead links were removed, and 45 email-tracking links were rewritten to their real destinations. Every old URL got its own permanent redirect, with no catch-all to the homepage.

Inside the project

Select a screenshot to view it full size.

The outcome

  • pft.org now runs on the new platform, replacing the Drupal site. The staff-rep lookup, endorsements and homepage are edited from the admin area, and the public membership figure, once overstated by about 360 in 7 hand-typed places, now comes from the roster at every build.
  • 260 archived press releases and PD posts (2008-2026) and 337 images and PDFs restored with their original dates, 47 dead links removed, and 286 permanent redirects so links in old coverage still land on the right page.
  • Lighthouse accessibility up from 92-96 to 100 on all 7 key public pages, SEO at 100 on all 7, and mobile performance of 92-98.
  • Audience counts update in 203 ms instead of 805 ms (75% less time, about 4× faster) as staff combine school, job, bargaining unit, dues type and 5 levels of legislative district.
  • 755 automated tests, plus lint, type checks and a production build, run in CI on every pull request. A database authorization suite runs against the live database on every merge, and every pull request went through automated code review, with each finding checked against the code before it was acted on.

Built with

Have a project like this in mind?

Tell us what you're building. We'll show you how we'd approach it and what it would take, the same way we did for Philadelphia Federation of Teachers.