It is your database, in your account, under your policy.
Podium deploys to a Cloudflare account in your name. Nobody at this project can read your speakers' data, because nobody at this project has it. Below is what the software does with it, and at the bottom, what a review found, what it never read, and what this project still does not have.
Redaction is the default, not a feature.
Adding a field means deciding whether it is personal data before it can ship. That is a rule with a check behind it.
Redacted unless asked for, everywhere
Email addresses, phone numbers, dietary and accessibility notes, travel details, and any answer a form marks personal need their own permission. Without it they are removed, including from a scope that can already read the proposal.
Never in a log line
Application logs carry identifiers, not values. A person is an id, so an email address cannot leak from a log aggregator, a crash report or a support screenshot.
Never in the published schedule
The public snapshot carries titles, times, rooms, speaker names and bios. Personal fields are not redacted from it at render time; they never enter it.
Two things nobody can unlock
Private notes about a person are never visible to that person, and neither is the review data for a proposal they submitted or are credited on. No permission lifts either.
Export and erasure are features, not a support process.
A speaker asking to be removed should not cost you an afternoon and a query you write by hand.
Export
Any person pulls everything held about them as JSON, plus their uploaded files, from their own portal. Nobody has to ask you, and you do not have to build it.
Erasure that reaches the copies
Erasure anonymises the record, clears the profile and deletes the files, keeping the proposal and session content they authored. It also deletes their rows from every linked external table, paused ones included. An erasure that stops at your database is not an erasure when half the programme is mirrored into a spreadsheet.
Anything that cannot be honoured is said out loud
If a provider refuses a delete, the link is left in an error state and surfaced. Nothing reports an erasure that did not fully happen.
Retention with a number on it
Abandoned drafts are purged after an event closes, two years by default. Audit and event logs are kept for seven years, personal data redacted after two.
Six properties the rest of it rests on.
Permissions are recomputed on every request
Nothing caches authority. A revoked role takes effect on the next request, not the next sign-in.
Blind review is enforced by refusal
In a single- or double-blind round, identity is withheld, not hidden with CSS. A reviewer with a declared conflict does not get a warning; the score is refused.
API keys are narrow and mortal
Scoped to named events, read separated from write, personal data its own additive scope, an expiry, a rate limit, and a secret shown once. A key for your marketing site cannot read a review.
Credentials are references, not values
An integration stores the name of a secret, never the secret. It is resolved at call time, which keeps a provider token out of the settings screen, the audit log and any export.
Everything that matters is audited
Who did what, when, from where. Append-only, and surviving an erasure with the identifier alone. Overrides and waivers carry a written reason, so a proposal accepted below quorum has a sentence beside it a year later.
One writer for the schedule
Two organizers dragging the same slot queue behind one another instead of interleaving. A stale edit anywhere else is refused with the current state.
What a review found, and where it stopped.
Nobody had read this product as a whole for security until August 2026. The review was internal: our own people on our own code, not an outside audit. It found six defects, all fixed, the five below each with the test that would have caught it. It was also a first pass. Two areas were counted and never read.
- Bytes somebody uploaded are never served back as something a browser will run — screened on the way in and again on the way out, so files stored before the fix are covered too. A speaker's headshot was the way in.
- Every response declares where scripts may come from, and inline script is not one of the answers. Nothing may frame the app or guess at a file's type. The schedule embed is the one deliberate exception, because being put in someone else's page is its job.
- The session cookie travels only over https and is unreadable to scripts.
- A sign-in link can only send you onward to a page inside your own deployment.
- Password guessing is capped — ten failures against one address, fifty from one source, in a rolling fifteen minutes — and refused before the password is checked at all, on every screen that checks one.
A sixth finding took a plan change rather than a line of code, and it is now closed:
Passwords were hashed below the recommended work factor while the deployment sat on Cloudflare's free plan, which allows ten milliseconds of processing per request. That was roughly two orders of magnitude cheaper to attack offline than it should have been, and it was disclosed here for as long as it was true.
The plan changed and the parameters went back: 12 MiB of memory, three passes, on OWASP's own list for this algorithm. A hash costs about 108 ms. An old hash is carried up the next time its owner signs in, so nobody was reset and nothing was migrated.
Two things still hold the line in front of it. Guessing at the sign-in form is capped and refused before the password is checked at all, and password sign-in is off by default in production, with invitations the intended route in.
Two areas the review never read
Scoping to your organization happens once, in the layer every query is meant to go through. Thirty-six go around it and answer for their own scoping. They were counted. Nobody read them one at a time, and one missed scope there is one conference reading another's data.
Holding a permission is not the same as owning the record you point it at. A handler that checks "may this person edit proposals" and then trusts the id in the URL passes review. Nobody has walked the routes where permission comes from a relationship: a speaker on their own session, a sponsor contact on their own account. No tool finds this one.
Separate work on API keys found two more authorization defects after that pass closed. A read-only key was refused every read it was granted. A key scoped to onboarding tasks could mint a second key that read personal data. Both are fixed.
What this project does not have
- No SOC 2, ISO 27001 or any audited compliance programme
- No third-party penetration test
- No security team, and no disclosure SLA — report it in the issue tracker
- No hosted service, so no vendor to answer your questionnaire
What you have instead
- The whole source, and the specification it was built against
- Every rule written down before the code, and checked against it in CI
- All 510 URLs in the app checked on every build; a new one with no permission check fails rather than waiting for the next review
- Your own account, your own region choices, your own backups
- The ability to have your own people review it before you deploy it
The implementation notes, and the hashing decision in full →
