CommunityPay holds the financial system of record for community associations — assessments, bank accounts, disbursements, and governance records. This page describes the controls that protect that data, and that money, in production.
Every control here is enforced today. Three of the figures below — the guard count, the database rules, and the verified statutory thresholds — print from their sources at /verify/platform/, each with a digest and the date it was read.
Fund Custody
The largest financial risk in association software is the platform sitting in the middle of the money. CommunityPay does not.
- CommunityPay never custodies association funds. Resident payments settle directly into each association's own Stripe Connect balance and disburse to its vendors. The platform account is not in the funds flow.
- Card numbers never touch our systems. Stripe tokenizes payment cards in the browser. No card, PAN, or CVV field exists anywhere in our data model.
- What reaches a vendor is limited by the approval, the wait, and a transfer limit. A bill's approval binds the amount, the vendor, and the fund, and on the transfer rail the bank it will be paid to, and a payment spends that approval. A changed vendor bank waits three business days before the rail pays it. Every vendor payment sent by transfer and every top-up is emailed to the whole board. On the processor rail, no single vendor payment, and no 24 hours of them in total, may exceed the association's transfer limit, which is its payout buffer target (five thousand dollars where none is set); changing it takes a fresh authenticator code. The same target sets when surplus sweeps to the association's own bank.
- Each association is isolated. Every association has its own connected account with independent bank verification and identity checks. One association's payment credentials cannot be used for another.
- Regulatory scope is limited. Because we neither hold nor transmit funds, we operate as a software platform, not a money transmitter. Custody creates regulatory risk. We avoid it entirely.
For security and operational reasons, we do not publish processor routing or rail-selection details publicly. Those are available under NDA during diligence.
The Association's Bank
An association's dues settle into its own connected account at the processor and sweep to its bank. Which bank that is, and who may change it, is the control that matters most.
- One pinned payout bank. Every bank on the association's processor account is recorded with a fixed role — the association's payout bank or a vendor's destination — and exactly one is pinned as the payout bank. A vendor's bank cannot be the pin; the database refuses it.
- The pin is checked against the processor every hour and on every bank and payout event. If the processor's default bank is not the pinned bank, automatic payouts pause, and a person has to restore the default and release them. Sweep entries post only for payouts to the pin.
- A bank PIN that belongs to the association, not to a login. It is checked on the server, never in the browser. Changing it takes the current PIN and a fresh authenticator code. Five wrong tries lock it. Staff cannot set it.
- Changing the payout bank takes two board members. One asks; a different one confirms, with the PIN and a fresh code. The board is emailed. The first payout to the new bank is held until a person releases it.
- The first payout bank needs payment authority in the association and an authenticator code. There is no staff shortcut, and a bank that matches a vendor's bank on file is refused.
- A new association connects a bank only after its signup is approved.
Vendors and Payments
- A vendor's bank is attached at its first payment, paid by its stored identifier, and removed from the processor after the payout settles plus seven days — at once if the vendor is deactivated or its bank changes. The rail refuses an inactive vendor.
- A changed vendor bank waits three business days before the rail pays it. A board member other than the person who changed it can end the wait early, after calling the vendor at a number the association already had, with a fresh code and the bank PIN. The confirmation is recorded and belongs to that one change; a later change waits again.
- Every vendor payment sent by transfer and every top-up from the association's bank is emailed to the whole board.
- A bounced payout reverses its entry and reopens the bill. Nothing stays marked paid that was not.
- Vendor risk is a guard, not a report. For a vendor linked to a contractor licensing record, debarment, license status, license expiry, insurance expiry, and active contractor strikes are read before the payment posts.
- Reserve money leaves the reserve only by a board-approved transfer. Each fund balances on its own, and a reserve bill is paid online only after the transfer that funds it has posted.
- Emailed invoices are read, then checked. Each association has its own address for invoices. Two readings of each document feed 23 checks — the vendor, the accounts, duplicates by number and by document, near-duplicates, the recurring schedule, dates, currency, amount limits, the bill-to name, the certificate of insurance, the submitter, the policy — and the outcome is submitted for approval, drafted, or held with one plain sentence. A request to change where a vendor is paid holds the invoice and sends one text. The inbox never approves and never pays.
Who May Act
- Only the association's admin changes, demotes, or removes a member who holds authority. Staff cannot. Staff set up new associations and nothing more.
- A resident account holds no ability. No admin role, no seat, no title: nothing to grant and nothing to steal.
- Granting or removing authority takes a fresh authenticator code, and both are emailed to the board.
- Money abilities start off, except for an association's first officer. Paying vendors, vendor bank details, the transfer limit, top-ups, and the payout bank are granted per admin, only by someone who already holds the ability, with a fresh code and an email to the board. An ability that is off is refused on the server, not hidden in the interface.
- Governance → Controls shows how the association is set up, on one page.
- Assessment schedules — what every unit is charged — leave an audit record on every create and edit. The board is told of any change, and billing every unit takes a fresh code.
Transport Security
All connections to CommunityPay are encrypted in transit via TLS 1.2+.
HSTS (HTTP Strict Transport Security) is enforced with the following production configuration:
| Setting | Value |
|---|---|
| HSTS Duration | 63,072,000 seconds (2 years) |
| Include Subdomains | Yes |
| Preload | Yes |
| SSL Redirect | Enforced |
Once a browser connects to CommunityPay, it will refuse plaintext HTTP connections for two years. This is not configurable by users or attackable via downgrade.
Content Security Policy (CSP) is enforced in blocking mode in production. Our CSP restricts script and connection sources to:
- Stripe.js (
js.stripe.com) for payment processing - Stripe hooks (
hooks.stripe.com) for embedded components - Approved CDNs for UI framework dependencies
- Our own API endpoints for data fetching
Inline scripts on application pages must carry a cryptographic nonce generated per request; scripts without a valid nonce are blocked by the browser. Public pages are served whole from a shared cache, where one nonce would be the same for every visitor, so they carry script hashes computed from the exact body sent instead. Two internal surfaces that inject scripts we do not control, the staff admin and the embedded charting routes, run a relaxed policy. All other external script sources are blocked. Inline styles are permitted where required by framework dependencies.
Data at Rest
Field-level symmetric encryption is used for stored credentials and bank account identifiers. Database credentials are loaded from environment variables and never stored in code.
Database connections require SSL in production, on managed infrastructure with automated backups. Public media files are stored in AWS S3. Sensitive documents — institutional packets, compliance reports, governance evidence, and violation records — are stored in a private S3 prefix with time-limited signed URLs (5-minute expiry). File downloads require application-layer authentication; the server generates a presigned URL and redirects the authenticated user — no file bytes are proxied through the application.
Permissions Policy restricts browser capabilities:
| Capability | Policy |
|---|---|
| Camera | Disabled |
| Microphone | Disabled |
| Geolocation | Disabled |
| USB | Disabled |
| Payment API | Self + Stripe only |
Tenant Isolation
A management company may oversee hundreds of associations on one platform. None of them can see another's data.
- Scoped at the data layer. Every record carries its association, every query is scoped by it, and foreign-key constraints hold the boundary at the database, so a record cannot drift between communities.
- Enforcement is per association. The enforcement engine is instantiated for one association at a time. Decisions for one community cannot reach another's ledger.
- Rules are the board's. Who may approve, who may pay, the transfer limit, and the fund policies are set per association, and the settings in force are stored with every decision they governed.
Enforcement Architecture
Every financial entry is evaluated by 18 enforcement guards before it can post. Eight database triggers hold the ledger's most critical invariants — a decision existing at all, journal balance, the lines of a posted entry, and both legs of a posted reversal among them — with 29 further database constraints, not just in application code a bypass could route around. A closed period is locked by a guard, in the application, on the posting date.
- A database constraint trigger refuses any entry with no enforcement decision attached. This holds regardless of what wrote the row — a background job, a console session, or a future maintainer who never knew the rule existed. An entry with no decision cannot come into existence.
- Every decision is immutable and logged before the entry persists. The dispatcher records its answer — allow, block, or override — and that record cannot be altered after the fact.
- A nightly probe reads the database's own catalog to confirm all eight database-level rules are actually installed and enabled, not just present in application code. A rule that's been silently disabled at the database level would be invisible to every other check; this is the one that isn't fooled by that.
- Overrides are a recorded decision, not a bypass. The guards that can be overridden require a logged authorization, and writing one takes a fresh authenticator code; the ones that can't have no override path at all.
Authentication & Session Security
CommunityPay enforces strict session controls:
- Session timeout: 60 minutes of inactivity
- Browser close: Sessions expire when the browser closes
- HttpOnly cookies: Session and CSRF cookies are inaccessible to JavaScript
- SameSite: Lax in production (required for OAuth flows)
Brute-force protection:
- 5 failed login attempts triggers a 30-minute account lockout
- Lockouts are IP-based, preventing credential stuffing attacks
- All authentication events are logged
Two-factor sign-in uses an authenticator app, with recovery codes issued at enrollment. An account that holds no payment authority may also sign in with a code sent by text message, and a remembered device skips the sign-in challenge for 30 days:
- Once a person enrolls, every session on that account must pass the second factor, whichever door it came through — the application, the admin, the CRM, portal logins, magic links, and OAuth callbacks. A session that predates enrollment is caught on its next request.
- A fresh code is asked for again, within the last ten minutes, before any action that moves money or decides who may: paying a vendor by transfer, changing a vendor's bank, changing the transfer limit, pulling money from the association's bank, reactivating a vendor, giving or removing authority, approving a bill, changing who may approve and pay, writing an override, changing a fund policy or the approval workflow, adding or changing the payout bank, changing the bank PIN, releasing the first payout to a new bank, confirming a vendor's new bank, changing an assessment schedule, and billing every unit. Paying several bills in a row needs one confirmation.
- Only the authenticator or a recovery code confirms a money action. A code sent by text message never does, and a remembered device does not skip the fresh code.
- A person with no second factor is sent to enroll before any of those actions. A new authenticator cannot confirm a money action until the account's owner approves it from an email sent to the account's address; rejecting it from that email removes it and locks the account. The owner is emailed when a factor is turned off.
- Enrollment is not yet required of every account. It is required, in practice, of anyone who does the actions above.
Audit Logging
Every significant action in CommunityPay is captured in an immutable audit log.
Immutability enforcement: Audit log records cannot be updated or deleted. Any attempt to modify or remove an existing record is refused at the application layer — not just by convention.
Checksum verification: Each audit log entry carries a SHA-256 checksum over its stored content, taken when the row is written. verify_integrity() answers one of three ways: verified, failed, or unverifiable. Checksums are versioned. Rows written since August 2026 are version 2: the hash is recomputed from the stored row, including its timestamp, and compared to the stored value. Rows written before that are version 1: their hash was taken before the timestamp was assigned, so no recomputation can reproduce it. Those rows are labelled unverifiable and are never re-stamped. History is not re-attested after the fact; it is monitored forward. A report-only management command counts the three states and exits non-zero on any failure. There is no repair path — a verifier that can rewrite checksums is not a verifier.
Coverage: The audit system tracks 54 event types across categories:
- Financial: Payment creation, disbursement execution, journal entry posting
- Authentication: Login, logout, password changes, failed attempts
- Authorization: Permission changes, role assignments, approval decisions
- Security: Suspicious pattern detection, rate limit violations
Request-level logging: The audit middleware captures every POST, PUT, PATCH, and DELETE request with IP address, user agent, and response context. Request logging flags common injection and path traversal signatures for investigation. SQL injection is prevented at the ORM layer through parameterized queries.
Sensitive data masking: PII and credentials (passwords, tokens, SSN, account numbers) are redacted before reaching error reporting services.
Taking the Books With You
An association's books are the association's. From Reports, Books Export downloads them as one zip, scoped to a fiscal year, a window, or the whole book: the general ledger, the journal register (every entry in every status, with the enforcement decision id recorded against each entry), the trial balance, the receivable and payable agings, the subledger schedules an auditor asks for by name, and the master data the ledger refers to — the chart of accounts, the funds, the units and their ownership percentages, the vendors, and the bank accounts. A README states what each file holds and what the export does not contain; a manifest carries the scope, the row counts, and a SHA-256 for every file including the README; and one digest over the manifest identifies the delivery. Anyone holding the zip can recompute every hash and check that debits equal credits without CommunityPay, an API key, or an account. No owner, resident, board member, or account signatory is named in any file. Corrections appear as reversal entries, never as edits. The export is gated by the same permission as report exports.
Consent & Third-Party Access
An association's data belongs to the association. No outside party reads it without the association's explicit, revocable permission.
- Consent-gated risk data. No lender, insurer, or title company can query an association's risk index without active consent from that association. Consent is revocable, and revocation takes effect immediately.
- Scoped API keys. API keys grant only the specific permissions assigned to them. A key issued to read a score cannot generate a report.
- A CPA's portal is read-only and scoped. It covers the period under review and answers only from the addresses the association lists. The link is shown once, the token is stored as a hash, and every access is on an events page.
- Keys and tokens stored as hashes. API keys and CPA portal access tokens are stored as SHA-256 hashes with a short lookup prefix. The raw secret is shown once at creation and never stored.
- Every access logged. Data access — and denied attempts — are recorded, giving the association a record of who looked at what, and when.
Webhook Verification
CommunityPay receives webhooks from Stripe, Dwolla, and Plaid. Every webhook is verified before processing.
Stripe webhook verification:
- HMAC-SHA256 signature validation against the
Stripe-Signatureheader - Replay attack prevention: signatures older than 300 seconds (5 minutes) are rejected
- Timing-safe comparison via
hmac.compare_digest()to prevent timing attacks
Dwolla webhook verification:
- HMAC-SHA256 with constant-time signature comparison
Rate limiting prevents webhook flooding:
| Provider | Rate Limit |
|---|---|
| Stripe | 100 requests/minute |
| Dwolla | 60 requests/minute |
| Plaid | 50 requests/minute |
Payloads exceeding 512 KB are rejected before processing.
Request Pipeline
A request first meets response compression, the shared page cache, the cache-header layer, and the crawler throttle. A cached anonymous page is served from there and does not reach the layers below. Every other request then passes through the same chain of independent layers, in this order: transport security headers, the browser capability policy, the additional response headers, domain routing, the session, the content security policy, cross-site request forgery protection, brute-force lockout, authentication, the second-factor gate, frame denial, webhook rate limiting, audit logging, and session audit.
Each layer operates independently. A failure in one does not bypass the others.
What We Don't Do
- We don't store raw credit card numbers (Stripe tokenization handles this)
- We don't store plaintext passwords (salted, iterated hashing)
- We don't custody or transmit association funds (payments settle into each association's own connected account)
- We don't let a vendor's bank become an association's payout bank
- We don't pay a changed vendor bank for three business days, unless a second board member confirms it by phone
- We don't send a vendor payment, or a day of them, above the association's transfer limit
- We don't accept a text-message code to move money
- We don't allow public access to sensitive document storage (signed URLs with 5-minute expiry)
- We don't permit inline scripts on application pages without a per-request cryptographic nonce
- We don't permit external script sources outside our CSP allowlist
- We don't log sensitive field values in audit trails
- We don't share association data with third parties without active, revocable consent
Security Contact
Report security issues to [email protected]. We investigate all reports and will respond within 48 hours. Our security contact is also published at /.well-known/security.txt.
This de
How CommunityPay Enforces This