Security & Trust

Taxerity.AIsecurity & trust posture. Seven sections — implementation details, limits, and open review.

The page names authentication, access, encryption, consent, session, and incident- response code paths. Deployment configuration and qualified security, privacy, tax, and legal review determine whether those controls satisfy your requirements.

  • Each section names a specific code path — src/lib/business/encryption.ts, src/lib/csp.ts, src/lib/auth-config.ts — the live control, not a marketing claim.
  • Anonymous-readable — review the security posture end-to-end before signup, with no auto-evaluation of your firm's data, no fields, no tokens.
  • Trial behavior follows the configured application paths; verify the deployed controls before using taxpayer data during a trial.

Section 01 · Encryption at rest

Encryption paths are documented with their boundaries

The repository documents encryption-related fields and admin-only key rotation; this is not a certification or a promise that every payload is covered.

The current implementation has an app-layer AES-256-GCM envelope for selected fields before database persistence. The reviewed paths include Client.taxId, Communication.body, and DeductionSuggestion.reviewNote; this is not a claim that every sensitive or uploaded payload is covered.

The active 32-byte key lives in env.SECURITY_ENCRYPTION_KEY (base64); the active key version is tracked in an EncryptionKey manifest row in the app schema, not pinned only by env var. When ops rotates, a new EncryptionKey row with version=N+1 is written and the prior row is stamped supersededAt=now(); old ciphertexts stamped v=N continue to decrypt under their own row manifest. If a ciphertext stamped with a superseded version outlives SECURITY_ENCRYPTION_ROTATION_GRACE_DAYS (90 days), decryptField raises a typed EncryptionUndecryptableError(grace-expired) — no silent fall-through to plaintext and no fall-through to the active key. A version the system has never seen throws unknown-key.

The app does not claim that every stored or uploaded payload is covered by the same field-level path. Review storage, provider, and deployment controls before using taxpayer data.

What this control actually does, in practice
  • Field-level, not table-level. AES-256-GCM envelope on Client.taxId, Communication.body, DeductionSuggestion.reviewNote — three columns today, each with its own IV + auth tag.
  • Version-tracked key manifest. Active key version pinned in an EncryptionKey row; rotations add a successor and stamp the predecessor supersededAt=now().
  • 90-day rotation grace window. Old ciphertexts stay decryptable across SECURITY_ENCRYPTION_ROTATION_GRACE_DAYS; beyond that, a typed grace-expired error — never a silent fallback.

Section 02 · Encryption in transit

Browser and transport headers are implemented in the shell

The shell configures transport and browser security headers. A qualified security review is still required for the deployed environment.

The repository configures browser-facing headers including HSTS and CSP. Actual transport, hosting, and third-party connector behavior depends on deployment and provider configuration.

Per-request CSP nonce + frame-ancestors none are wired in proxy.ts (Next 16 proxy, no middleware.ts): the proxy generates a fresh nonce via crypto.randomUUID per request, builds the policy via buildCsp in src/lib/csp.ts (script-src nonce + strict-dynamic, no unsafe-inline/unsafe-eval in production), and pins frame-ancestors 'none'. X-Frame-Options: DENY rides alongside as defense-in-depth for older browsers (CSP frame-ancestors obsoletes it per OWASP, but pinning both is the lower-risk posture for a marketing surface that embeds no third-party widgets).

Permissions-Policy (built from appCapabilities in next.user-config.ts), Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Resource-Policy: same-origin, Referrer-Policy: strict-origin-when-cross-origin, and X-Content-Type-Options: nosniff are all pinned in next.config.ts so a deployment can never silently lose one of them — every header is explicit and asserted at build time.

What this control actually does, in practice
  • HSTS pinned. Strict-Transport-Security max-age=63072000; includeSubDomains; preload in next.config.ts.
  • Per-request CSP nonce. crypto.randomUUID() per request → script-src nonce + strict-dynamic; no unsafe-inline / unsafe-eval in production.
  • frame-ancestors none + X-Frame-Options: DENY. proxy.ts pinned in src/lib/csp.ts; no public surface of Taxerity.AI can be iframed by another origin.

Section 03 · Authentication & access control

better-auth sessions and role-gated admin

Passwordless magic-link or email/password sign-in; every per-user /api/* route is gated by requireAuth(); admin pages route through requireAdmin().

Taxerity.AI sign-in runs through better-auth (src/lib/auth.ts) with two modes — passwordless email magic-link and classic email/password — both wired in src/lib/auth-config.ts. There is no anonymous-write path: every per-user /api/* route handler starts with let user; try { user = await requireAuth(req); } catch (res) { return res as Response; } so a missing or expired session returns a 401 without redirecting the fetch. Every per-user query in those handlers is scoped where: { userId: user.id } so a session token from one user cannot reach another user's clients, returns, communications, or suggestions.

Admin access is its own boundary. requireAdmin() (src/lib/require-admin.ts) redirects any signed-in user whose better-auth role is not admin back to /login or /, so the admin dashboard and the key-rotation endpoint sit behind a single role check — never a hard-coded password, never a custom admin cookie, never a shared secret. Key rotation itself is admin-only and audit-logged: it writes a new EncryptionKey row, stamps the predecessor supersededAt=now(), and emits a DataAccessAuditEvent of kind EXPORT against the encryption_key resource so the rotation is itself an audit-relevant action.

The auth configuration includes session expiry and admin gating. MFA is not enabled for every account by this application; do not represent the current configuration as universal MFA or a complete security posture.

What this control actually does, in practice
  • passwordless magic-link + email/password. Two sign-in modes through better-auth; both surface the same requireAuth() gate at the API layer.
  • Per-user /api gate + per-user query scope. requireAuth() at the top of every protected route handler; every query scoped where: { userId: user.id }.
  • Role-gated admin. requireAdmin() redirects non-admin users; key rotation writes a new EncryptionKey row and emits an audit event.
  • Session configuration. Session expiry and update behavior are configured in the application; review the deployed settings before making a security commitment.

Section 04 · Data segregation per firm

Firm-scoped tenancy — one firm cannot read another firm's clients, returns, or audits

Firm + FirmMember { OWNER | MEMBER } drive every cross-firm boundary; trial engagements sit under the same poster as paid ones.

The application uses firm and member records plus scoped route queries for the reviewed data paths. Automated tests cover selected cross-firm cases; the release report lists uncovered surfaces and the remaining review.

A trial or membership record is not a blanket guarantee for every route, connector, or deployment. Review the current evidence before using real taxpayer data.

Membership is the control, not the URL. A user cannot "visit" another firm's workspace by guessing a path — the route handlers, the data plane, and the access-audit layer all read authorship from FirmMember, so the boundary is enforced at three layers, not just painted onto the UI.

What this control actually does, in practice
  • Firm + FirmMember { OWNER | MEMBER }. Per-firm tenancy via prisma/schema/billing.prisma; @@unique([firmId, userId]) supports the reviewed membership boundary.
  • Per-user query scope. Every protected handler scopes by firmId (rows that carry one) OR userId (per-user rows); the two paths never cross.
  • Trial parity. Trial behavior follows the configured application paths; verify the deployed controls before using taxpayer data during a trial.

Section 05 · Treasury Circular 230 §10.35

Professional responsibility stays with the practitioner

The tool sits inside those rails. The firm's signer stays on the line, signed under the firm's PTIN.

Treasury Circular 230 §10.35 puts the duties of confidentiality, competence, diligence, and appropriate supervision on the practitioner — the firm's signer under the firm's PTIN, not the tool. Taxerity.AI is a drafting helper; the return that ships is your firm's return, signed by your signer, under your EFIN and PTIN. Confidentiality of taxpayer information is therefore a practitioner duty the platform must support, not own: the access controls above (firm-scoped tenancy, field-level AES, per-firm access logging) exist so the practitioner can answer §10.35 from evidence rather than from policy.

Per-engagement consent is captured before any pull. Every pull runs through a counsel-on-record row in the audit log against the engagement — not after the fact, not retroactively. If a member of your firm has not been granted access to a client, they cannot read the client; if a member no longer works for the firm, their FirmMember row is the gate that closes access without any data migration.

Taxerity.AI does not sign or file a return, provide professional approval, or replace the practitioner’s judgment. Circular 230 and other professional obligations remain with the practitioner.

What this control actually does, in practice
  • Practitioner of record stays on the line. The tool produces the draft; the firm's signer signs the return under the firm's PTIN. No auto-file, no IRS write.
  • Per-engagement consent before any pull. Every pull begins with a counsel-on-record row in the audit log against the engagement — never a quiet pull, never retroactive.
  • Firm-controlled access. FirmMember { OWNER | MEMBER } is the access boundary, so a departed staff user cannot keep reading once their membership is removed.

Section 06 · Security controls for review

Implementation details with open security review

The repository documents selected access, encryption, and audit paths; this page is not a certification or safeguards conclusion.

Applicable tax-privacy and security requirements depend on the firm, deployment, and service terms. The application exposes selected code paths for access checks, sensitive-field handling, and audit events; qualified advisers must determine whether those controls are sufficient for a proposed use.

Implementation evidence includes better-auth session and role checks, firm-scoped queries, selected encryption helpers, and DataAccessAuditEvent records. Availability, configuration, coverage, retention, and certification status must be verified separately.

The product does not represent these implementation details as an independent attestation, legal conclusion, or clearance for real taxpayer data.

What this control actually does, in practice
  • Documented review scope. This page identifies selected implementation paths; it does not claim to satisfy every external security framework or requirement.
  • Encryption at rest + in transit. AES-256-GCM envelope on sensitive columns; TLS 1.3 + HSTS pinned + frame-ancestors none across the request surface.
  • Access controls + monitoring. better-auth + role-gated admin + firm-scoped tenancy + per-firm DataAccessAuditEvent trail.
  • MFA and retention remain open. MFA availability, retention periods, and record sufficiency require current owner and professional review.

Section 07 · Incident response

Incident response — admin-only key rotation, per-firm access events, support contact

The rotation path is admin-only and audit-logged; per-firm access events feed the audit log; report an incident to the security team by mail.

Taxerity.AI's incident posture centres on four observable controls. First, encryption-key rotation is admin-only and audit-logged: rotating the active key writes a successor EncryptionKey row, stamps the predecessor supersededAt=now(), and emits a DataAccessAuditEvent of kind EXPORT against the encryption_key resource so the rotation itself is recoverable on request. Second, every read/write/delete of a sensitive client record runs through auditFn() in the dataset's resource route, producing a DataAccessAuditEvent row that captures who looked at which client row, when, and against which resource — recoverable as evidence under exam or state-board review.

Third, every taxpayer-data handler scopes by firmId through requireFirmMember (Phase 5 hardening) — userId-only scoping is replaced by FirmMember.userId resolution so a user who leaves one firm cannot keep reading through a stale cookies or URL. Section 7216 acknowledgement toggles on FirmConsent row are audit-logged; the resolution of a portal token is the source-of-truth for which client it serves, never the request body's stock clientId. Fourth, the soft-archive chain delivers the deletion-and-retention evidence a departing engagement can be examined against: the DELETE handler on /api/clients/[id] stamps Client.archivedAt, revokes any active ClientPortalToken row in one transaction, and emits a DELETE-class audit row carrying the revoked-token count. A follow-up shred cron will hard-delete rows whose archivedAt is older than 30 days AND shred UploadedDocument.extractedData / Communication.body for the same client+firm before the table can be cross-joined.

Fifth, there is a support contact for reported incidents: mail the team and the operator can triage against available access-audit records. Key rotation and admin-role events are observable application paths; monitoring, response times, and service-level commitments require separate operational review.

Tracked ownership items include MFA wiring, key-management choices, coverage of sensitive fields, penetration testing, and any external security attestation. These remain open until the responsible owner or assessor records evidence.

If a real incident is observed — a stolen session, a leaked credential, or a suspicious access row — contact the team immediately. Do not treat this page as a guarantee about response time, certification, or clearance for real taxpayer data.

What this control actually does, in practice
  • Admin-only key rotation. New EncryptionKey row written; predecessor stamped supersededAt=now(); a DataAccessAuditEvent is emitted.
  • Per-firm access events + firm-scope. auditFn() on every sensitive read/write/delete; requireFirmMember() replaces userId-only scoping on every taxpayer-data handler.
  • Soft-archive on delete + retention cron. DELETE on /api/clients/[id] stamps archivedAt + revokes active portal tokens; shred cron (follow-up) hard-deletes >30d archived rows + wipes extractedData / communication body for the same client+firm.
  • Tracked owner items (not yet a control). MFA / key management / sensitive-field coverage / penetration testing / any external attestation — each remains open until evidenced and reviewed.
  • Single support contact. Mail the team at the address below; response handling and service levels require operational review.

Verify the posture on the related surfaces

Three pages to read next.

The seven sections above document the controls themselves; the three pages below document the commitments those controls enable:

  • /trust — the four-pillar review framing: practitioner responsibility, consent controls, selected security implementation details, hosting posture, and decision records.
  • /faq — six objections that decide whether a solo CPA signs up, including the data-residency, owner-review, AI-accuracy-under-exam, and AI-vs-ChatGPT questions.
  • /book-demo — a 15-minute walkthrough of the controls above on a real engagement, no deck, no commitment.

How this page is built

Seven sections, one const feeds the page and the JSON-LD.

  • · Source-of-truth: a single typed SECURITY_TRUST_PILLARS const, colocated in this file, feeds the rendered sections AND the JSON-LD WebPage mainEntity.
  • · Seven sections, one code path each: encryption at rest (encryption.ts), encryption in transit (csp.ts + proxy.ts + next.config.ts), authentication (auth-config.ts + require-auth.ts + require-admin.ts), data segregation (billing.prisma Firm/FirmMember), professional-responsibility review, security implementation details, and incident response.
  • · Lock-step with /trust: this page scopes strictly to the security lens — four-pillar compliance framing stays on /trust so the two pages do not duplicate commitments.
  • · Anonymous-readable: no auth gate. The page exists to address posture during outreach, before sign-up — gating it would defeat the funnel.
  • · JSON-LD: three hidden <script type="application/ld+json"> blocks — organizationGraph + softwareApplicationGraph + WebPage (one SecurityPosture envelope whose mainEntity is the seven sections). React renders JSON as a text child of <script> so there is no XSS surface and no need for dangerouslySetInnerHTML.
  • · Per-section anchors: every SecurityPillar.id renders as <article id={pillar.id}> so /security-trust#<pillar-id> deep-links survive hash scroll.
Questions about a specific control? Email the team.

Read next: a related engagement

IRS notice response for solo CPAs and EAs responding under their firm's PTIN.
Year-end tax planning for solo CPAs and EAs running a Q4 engagement under their firm's PTIN.
A 15-minute walkthrough of the notice-response workflow on a real engagement — no deck, no commitment.
Professional at $149/month for solo practices, Firm across the whole engagement team. 14-day free trial runs the full base platform end-to-end first.

See it on a real engagement →

A 15-minute walkthrough of the controls above — encryption in motion, role-gated admin, per-firm access logging, the consent capture flow, the incident path — on a real engagement, no deck, no commitment. Read more on /trust for the compliance framing and on /pricing for the trial tier.

Book a walkthrough →