Effective: May 21, 2026 · Last reviewed: June 3, 2026 · Owner: Cameron Betz (cameron@rinserightservices.com) · Reviewed annually at minimum.
This policy is approved by the business owner and is the authoritative reference for all information-security practices in RinseRight. Sections below address each domain of the program; URL fragments (e.g. /security#mfa) link straight to a given section for vendor questionnaires.
1. Scope and objectives
RinseRight is a single-operator residential and commercial exterior-cleaning business in Idaho. This policy applies to:
The RinseRight CRM web application and its production database.
All third-party processors integrated with the CRM (Plaid, Google Business Profile, Stripe, Twilio, Cloudflare R2, Resend).
The business owner's workstation as the sole production-access endpoint.
Objectives:
Protect the confidentiality, integrity, and availability of customer and financial information.
Maintain compliance with applicable privacy laws and processor requirements.
Limit insider risk via least-privilege access.
Ensure the business can recover quickly from incidents.
2. Accountability
The business owner (Cameron Betz) is the executive sponsor and the operational owner of this policy. All security decisions, exception approvals, and policy updates are signed off by the owner. Contact: cameron@rinserightservices.com.
3. Access control policy
RinseRight enforces role-based access control (RBAC) in the application layer combined with the principle of least privilege. Each user account is bound to exactly one of three roles:
ADMIN — the business owner. Has all permissions. Required to access financial integrations (Plaid, Stripe, tax forms), payroll, audit logs, system settings, and team management.
SALES— quote-builders. Can view and edit clients, jobs, quotes, invoices, and reviews. Cannot access payroll, tax integrations, system settings, or other users' commissions.
TECHNICIAN— field crew. Can view today's assigned jobs, check in/out, log tasks, and request edits. Cannot see pricing, other techs' schedules, or any financial data.
Additional fine-grained permissions (overrides on a per-user basis) are stored as a JSON array on the User record (permissions column) and checked at every server route via requirePermission(). ADMIN implicitly satisfies all permission keys.
New-user provisioning: the owner invites a new user with one of the three roles. Until they verify the invitation, their status is PENDING_ACTIVATION and they cannot log in. Access changes require an admin to edit the User record from the Team page.
4. Multi-factor authentication standard
MFA is mandatory for all users with ADMIN role and is enforced before any admin-gated route — including the Plaid Link integration and bank-feed inbox — becomes reachable.
Approved MFA methods:
Time-based one-time password (TOTP) via an authenticator app (Google Authenticator, Authy, 1Password, etc.). This is the standard method for ADMIN accounts.
Email-based 6-digit code — available as a fallback for non-ADMIN users without a TOTP-capable device. Not approved for ADMIN access to financial systems.
Both methods exceed the minimum “robust MFA” definition used by our payments and banking processors (Plaid, Stripe), which expressly recognizes TOTP and per-session codes as robust forms.
External account hardening: the RinseRightFly.io account, Plaid dashboard, Stripe dashboard, and GitHub repository all enforce MFA on the owner's account. Recovery codes for each are stored in a password manager on a hardware-encrypted device.
5. Identity and access management
At single-operator scale, RinseRight does not subscribe to a third-party identity provider (Okta, Auth0, JumpCloud, etc.). The CRM provides a centralized identity and access-management surface through the following components:
Single sign-on at the application layer — NextAuth manages credential storage (bcrypt-hashed passwords), session lifecycle (30-day rolling), and JWT issuance. Every authenticated route validates the session against the database on every request.
Single user directory — the User table is the only source of identity in the CRM and is re-validated on every authenticated request via requireAuth(). A user disabled in the database is logged out within one request cycle.
Centralized permission registry — permission keys are defined in one place (lib/permissions.ts) and consumed at every access point. There is no role or permission stored outside the User row.
Centralized session log — successful sign-ins, sign-outs, and sensitive admin actions are appended to the AuditLog table. Reviewable from /admin/audit.
If RinseRight grows beyond ~5 users with production access, the policy review process will re-evaluate the adoption of a hosted IAM provider with SAML/OIDC support. Trigger condition: any addition of staff requiring long-term production-data access beyond the current owner-led model.
6. Zero-trust posture
RinseRight operates under the core principles of zero trust: no implicit trust based on network location, continuous verification of identity, and minimum-privilege access. The architecture implements these principles as follows:
No internal network.The CRM runs on Fly.io with no VPN, no private corporate network, and no implicitly-trusted IP ranges. Every request, including requests from the owner's home IP, traverses the public internet and must authenticate via TLS-encrypted HTTPS plus a valid session token.
Continuous identity re-validation.requireAuth() re-loads the User row on every authenticated request and immediately revokes access if status has changed since the JWT was issued. A user disabled at 10:00:00 is locked out at 10:00:01.
Per-route authorization. Each protected route calls requirePermission(“key”)with a specific permission key. There is no “logged in = trusted” shortcut anywhere in the codebase.
MFA gate on sensitive paths. All admin routes — including the Plaid Link surface — require MFA to be completed in the current session before the page renders.
Service-to-service authentication. Webhook callers (Twilio, Plaid, Stripe) authenticate via cryptographic signature verification (HMAC-SHA1 for Twilio, signed payloads for Stripe) before any data is written. Cron endpoints require a shared secret.
Encrypted everywhere. TLS 1.3 in transit; volume encryption at rest; AES-256-GCM on sensitive credentials at the application layer.
7. De-provisioning
When a user's engagement with RinseRight ends or changes, access is revoked or adjusted automatically via the following enforced mechanism:
Termination.An admin sets the user's status to DISABLED from the Team page. The next request that user issues is intercepted by requireAuth(), the user is redirected to /login?reason=inactive, and their session token is invalidated. No manual session purge is required because the JWT is verified against User.status on every request.
Role transfer.Admin updates the role or permission JSON on the user's record from the Team page. The change takes effect on the user's very next request — they do not need to log out.
Trusted-device revocation.An admin can force-clear all of a user's “Trust this device for 30 days” entries from their own Security settings, or admins can reset another user's 2FA entirely from the Team page.
External account de-provisioning.When a user leaves: their access to GitHub, Fly.io, and any other operational tools is revoked through each provider's admin console as part of the offboarding checklist below.
Offboarding checklist:
Set CRM User.status = DISABLED from the Team page.
Reset their 2FA, forcing re-enrollment if they ever return.
Revoke any external-service access (GitHub collaborator, Fly.io member, Google Workspace if applicable).
Confirm in the audit log that no sessions remain active.
8. Periodic access reviews
RinseRight conducts an access review at minimum annually, with an additional review triggered any time a team member's role or engagement status changes.
Review procedure:
Open /team and confirm every User row is either the active owner or an active, currently-engaged employee/contractor.
For each active user, confirm the role and any fine-grained permission overrides match their current responsibilities.
Check /admin/audit for unexpected admin actions in the past 12 months (e.g. permission changes, user re-activations, password resets initiated outside a documented support flow).
Confirm all admin accounts have 2FA enabled (visible from the Team page).
Verify trusted-device counts; force-clear any device entries on an admin account if the device might no longer be in the owner's possession.
Re-confirm external accounts (Fly.io, Plaid, GitHub, Stripe) only list intended members and that recovery email addresses are current.
Document the review date and findings in this policy file's “Last reviewed” header.
9. Vulnerability management and patching SLA
RinseRight runs automated dependency vulnerability scanning continuously and patches identified vulnerabilities according to the following Service Level Agreement, measured from the moment a CVE is published or a vulnerability is otherwise disclosed to us:
Severity
Patching SLA
Action
Critical (CVSS ≥ 9.0)
Within 7 days
Out-of-band patch + deploy
High (7.0 ≤ CVSS < 9.0)
Within 30 days
Scheduled patch + deploy
Medium (4.0 ≤ CVSS < 7.0)
Within 90 days
Next routine dependency refresh
Low (CVSS < 4.0)
Best effort
Bundled with next major dependency upgrade
Scanning and detection:
GitHub Dependabot continuously scans the application repository for vulnerable npm dependencies and opens PRs for available patches.
npm audit is reviewed on every dependency change (i.e. every npm installduring normal development).
Fly.io rebuilds the production container from a patched base image on every deploy, picking up OS and runtime security updates automatically.
End-of-life software is monitored: the stack tracks current LTS releases of Node.js, Next.js, Prisma, and PostgreSQL/SQLite. The annual policy review includes a check for any dependency within 6 months of EOL.
10. Encryption
In transit. TLS 1.3 enforced via HSTS for all public traffic.
At rest, volume-level. Fly.io managed volumes are encrypted at rest with LUKS/dm-crypt by default. The entire production database file resides on an encrypted volume.
At rest, application-level (defense in depth). Plaid access tokens, Google OAuth tokens, EIN, and TOTP secrets are individually AES-256-GCM encrypted at the application layer using a key derived from a Fly secret (AUTH_SECRET) stored separately from the database file. The encryption key is never written to the database and is never logged.
Passwords. Stored as bcrypt hashes (cost factor 12) — never logged, never reversible.
11. Incident response
Reporting: Anyone who notices or suspects a security incident should report to cameron@rinserightservices.com immediately.
Containment procedure:
Set affected User.status to DISABLED to immediately invalidate sessions.
Rotate Fly secrets associated with the compromised path (AUTH_SECRET, third-party API keys).
For a compromised bank connection, click Disconnect on /admin/taxes/banks (revokes Plaid access token and deletes BankAccount + BankTransaction rows).
For a compromised GBP connection, click Disconnect on /admin/reviews.
Review the audit log for the affected user / time window to characterize impact.
Notify affected customers and applicable regulators within timeframes required by law.
12. Policy review
This policy is reviewed at least annually. Material changes are reflected in the “Last reviewed” date at the top of this page and noted in the version-control history of the application repository. Out-of-cycle reviews are triggered by:
Onboarding or offboarding a team member with production access.
Integrating a new third-party data processor.
A security incident or near-miss.
Material change in regulatory landscape (e.g. new state privacy law).