~/projects/pcm-proof-automation
Epsilon PCM Proof Automation
Replaced multi-minute manual proof triggers with a 10-second self-service form
- minutes → ~10 seconds task time reduction
- self-service, no PCM login needed access
Node.js · Express · Playwright · n8n · Gemini 2.5 Flash Vision · Docker Compose
Problem
Epsilon’s PeopleCloud Messaging (PCM) platform powers email campaigns across multiple business units. Triggering a proof email—typically done before launching—required:
- Multi-step Okta SAML SSO login via the internal PCM web UI
- Manually navigating to a campaign
- Pasting customer keys one at a time into a form field
- Repeating this per campaign, per proof cycle
The process took several minutes per proof, required direct PCM access, and left no audit trail. It couldn’t be delegated and blocked team members until confirmation arrived.
Constraints
The core architectural constraint: Okta SAML SSO cookies extracted from the login flow are rejected when re-used externally. This eliminated simple solutions like extracting cookies and passing them via HTTP headers.
The only viable approach: all PCM REST API calls must execute from within Playwright’s live authenticated browser context. The browser owns the active Okta session; each API call runs through context.request.* while the session is live. On expiry, a new login happens transparently, and the request retries automatically.
This constraint shaped the entire architecture: instead of a stateless HTTP service that could run anywhere, I built a stateful session manager that keeps a long-lived Playwright browser alive between requests, trading horizontal scalability for correctness.
Architecture & Decisions
The system runs as two Docker services:
Session manager (Node.js + Express + Playwright): Owns Okta login and all PCM API calls. It keeps a long-lived browser context alive, handling re-login automatically on expiry, and exposes a small internal REST API consumed by n8n.
n8n (workflow orchestration): A 3-page web form. Page 1 collects business unit, campaign UUID, customer keys (pasted or from a screenshot), and recipient emails. Page 2 shows a campaign preview and asks for confirmation. Page 3 returns a Ref ID and a direct link to a live status tracker.
The status page is a separate webhook-based HTML page that auto-polls the API, displaying COMPLETED / FAILED / IN_PROGRESS in real time. Fire-and-forget design: the trigger form returns immediately after submitting the proof; status checking is asynchronous and available on-demand.
Build Notes
Three decisions that consumed most of the build effort:
Screenshot OCR: Gemini 2.5-flash extracts customer keys from pasted images. Deduplication logic merges OCR-extracted keys with manually pasted keys. The endpoint is rate-limited and validates image headers before processing.
Session expiry and re-login: When a Playwright session expires, the next API call triggers a full Okta re-login. Requests serialize through a login queue to prevent simultaneous login storms. If two requests hit the session boundary, one waits for the other’s login to complete, then retries.
Daily append-only audit logging: Every proof trigger and status lookup is appended to a daily append-only log inside a Docker volume, with full context and timestamps per entry. Queryable via the internal API; tamper-evident structure.
Outcome
The system has been live in production since Phase 1 (May 2026). Task time dropped from several minutes to ~10 seconds for submission, ~60 seconds for full confirmation. Team members without PCM access can now trigger proofs via the form. The audit trail is complete and append-only, providing compliance-grade visibility.
Five phases of incremental feature delivery—OCR integration, campaign preview, status tracking, multi-BU support, and webhook-based status page—kept scope manageable and allowed early iteration feedback.
What I’d Do Differently
I’d separate the Playwright session manager into a proper service discovery layer earlier. Hardcoded Docker DNS service names work, but a more formal IPC boundary (gRPC or GraphQL) would make unit testing easier and decouple n8n from implementation details.
I’d also revisit the session-lifetime strategy. A shorter, stricter window might simplify the re-login logic, though it would add latency on the first proof per session. Monitoring actual session churn in production would inform the optimal setting.