Command: cat projects/edge-crm.md
A corporate site and an in-house CRM at the edge, with privacy as a requirement
A B2B group replaced an off-the-shelf CRM with its own system: a site in three languages that captures contacts through an interactive assessment and meeting booking, and a CRM with leads, a deal pipeline, a meeting queue, content and analytics. It all runs in a single Worker at the edge, with five access roles, owner-scoped data and Brazil's data protection law (LGPD) treated as part of the product.
open the demoa simulation of the system with fictional data · 6 screens
- Role
- Sole engineer, from product to operations
- Period
- Jun 2026 – Sep 2026
- What I did
Me: Product, architecture, data model, roles and access rules, every screen of the site and the CRM, tests, deployment, operations and the documentation for non-technical readers.
Private project, described without identifying the client, product or company.
Diagram
Everything runs in a single Worker at the edge. The institutional site is pre-rendered, and what arrives through its forms and meeting booking goes to D1. The CRM and the APIs run on demand, with one role ladder for the menu and the API and scoping by owner; they write to D1, which holds leads, deals, meetings and audience, and keep files and photos in R2. A daily job anonymizes and prunes expired data, and Workers AI produces translations that stay marked for human review.
Context
A B2B group focused on international expansion and governance used an off-the-shelf CRM. It needed a site in three languages that qualifies whoever arrives, and its own CRM for the sales team, with personal data handled under the LGPD from the very first form.
What I built
- The site in three languages. Pre-rendered corporate pages, an interactive maturity assessment that becomes a lead with the full questionnaire, landing pages with a multi-step form, a property catalog, a blog and meeting booking with slots computed from the calendar rules.
- The CRM. A dashboard with an action queue, leads with status and owner editable in the row itself, a Kanban deal pipeline with attachments, team tasks, a meeting queue, a block-based blog, properties, the people shown on the site, sales analytics and settings that change the site without a deployment.
- Translation with human review. When an article is saved, the other two languages are translated in chunks, never blocking the save, and stay flagged “to review” until a person reviews them.
- Privacy end to end. Consent on the server, anonymization and deletion under different roles, a daily retention job and cookieless audience measurement.
- Operations written for people. A runbook with rollback and a guide for non-technical readers.
What I would do differently
From the start I would put every “who is allowed” rule in a single function. People disappeared from the host picker because three files held three versions of the same rule; the fix became a single rule, with a test that proves it can fail.
Constraints
- The site and the CRM in the same edge Worker, with no server of our own to maintain.
- Personal data of leads, meetings and visitors under the LGPD, from consent to deletion.
- A single technical person to build and run it.
- Site figures and contacts editable by the team, without a new deployment.
Decisions
One Worker, with the corporate pages pre-rendered
- Context
- The site needs speed and SEO in three languages; the CRM needs sessions and a database. Two separate projects would double the deployments, domains and security configuration.
- Choice
- Astro with the corporate pages pre-rendered and the CRM and APIs rendered on demand in the same Worker, with a custom entry point only to add the daily retention job, which the framework adapter does not expose.
- Gains
- One deployment, one domain and a single set of security headers.
- Corporate pages served as files, at no server cost.
- Costs
- The custom entry point has to be reviewed on every framework upgrade.
One role ladder for the menu and the API, with owner scoping
- Context
- Five roles share the CRM. Hiding a menu item does not stop anyone from calling the API, and salespeople should only see their own leads.
- Choice
- A single role ladder decides what the menu shows and what each endpoint accepts. For Sales, reads and writes of leads and deals are filtered by owner.
- Gains
- The menu never offers what the API refuses.
- The dashboard stopped showing every lead's personal data to any role.
- Costs
- Every new screen has to declare its minimum role and its scope, and get tests for both.
Audience measurement without cookies
- Context
- The platform's traffic analytics did not count unique visitors, and a third-party tool would bring a cookie, a banner and data leaving the system.
- Choice
- A unique visitor is a salted hash of IP and browser. The IP and the browser are never stored, the salt is generated once and kept out of the interface, and the records live for 120 days.
- Gains
- The numbers the team needs, with no cookie and no personal data stored.
- Costs
- Someone who switches network or browser counts as a different person.
A meeting is confirmed only when a person takes it
- Context
- Automatic booking would promise a meeting with nobody to run it, and two simultaneous requests could grab the same slot.
- Choice
- A request starts pending and without a host, with the slot already locked in the database by a conditional insert and a partial unique index, colliding on overlapping ranges. The e-mail with the host's name, the room link and the invite goes out only when someone on the team confirms.
- Gains
- Visitors never get a promise nobody will keep.
- No double booking, even with simultaneous requests.
- Costs
- It depends on the team clearing the queue, so the queue sits at the top of the dashboard and flags slots that have already passed.
Anonymize and delete under different roles
- Context
- A data subject can ask for their data to be deleted, but the pipeline still has to add up.
- Choice
- Managers anonymize: personal data is erased and the record stays in the metrics. Admins delete for good, and the audit log keeps the metadata of what was destroyed. A daily job anonymizes unconverted leads and bookings older than 24 months and prunes the audit log, visitors and expired sessions.
- Gains
- Pipeline metrics survive a deletion request.
- Retention does not depend on anyone remembering.
- Costs
- Converted leads stay out of automatic anonymization until the company decides the window.
Stack and why
- Astro 5
- Pre-rendered corporate pages and an on-demand CRM in the same project.
- Strict TypeScript
- Role and retention rules typed end to end.
- Cloudflare Workers, D1 and R2
- Site, APIs and CRM in one Worker; SQLite database and files at the edge.
- Workers AI
- Translates the blog into the other two languages, always flagged for human review.
- Tailwind v4 and one React 19 island
- A token-based design system and JavaScript only in the interactive assessment.
- Vitest
- Unit tests, and E2E suites that run against the built Worker.
Results
Five roles on a single ladder used by the menu and the endpoints, with owner scoping on reads and writes.
audited private repositoryThe CRM's access code and screens(Audit of the private repository, Sep 2026)14 unit test files with about 200 cases, 5 E2E suites that run against the built Worker, and CI with type checking and tests.
audited private repositoryThe repository's tests and CI workflow(Audit of the private repository, Sep 2026)An adversarial review across seven dimensions, with every finding confirmed by refutation before it was queued, and 54 fixes in a single round.
self-reportedThe project's audit log
Security angle
Attack surface
- The site's public forms (assessment, contact, application and booking).
- CRM sessions with five roles.
- Deal attachments and property photos uploaded through the CRM.
- Personal data of leads, meetings and visitors.
Controls in place
- Opaque session tokens with only the hash in the database, and passwords hashed with PBKDF2 and a per-user salt.
- A same-origin check on every POST, and login attempt limits per e-mail and per IP.
- Consent required and timestamped on the server.
- Task links restricted to http and https, always showing the destination domain.
- Source maps turned off in production.
What I would test today
- A Sales session reading or changing someone else's lead straight through the API.
- Simultaneous requests for the same slot or for overlapping ranges.
- Searching for a lead's data after anonymization.
Evidence
- audited private repositoryAudited private repository(Audit of the private repository, Sep 2026)