Audits → Internal Audit
Purpose
Schedule and conduct a checklist-driven internal compliance audit using either a built-in default template or a clinic-custom template. Internal audits are the day-to-day compliance rhythm between biennials — typically run quarterly — and they're the single highest-leverage thing a clinic can do to walk into a real DEA visit with confidence.
This is general product information about CS Logbook, not legal advice. Internal audits are an operational best practice, not a CS Logbook-defined requirement. Verify any required review cadence with the DEA Diversion Control Division and your state veterinary/pharmacy board.
Regulatory basis: internal audits are not directly mandated by the DEA, but ongoing internal review supports the security and recordkeeping principles in the DEA security rules and Part 1304 recordkeeping. Some state boards may require periodic internal compliance checks — confirm your state's rules. (SOC 2 Trust Services Criterion CC4, Monitoring, also calls for evidence of ongoing control review.)
When to use it
- Quarterly compliance review — the most common cadence; pick a calendar quarter and run the same template every Q.
- After a near-miss — administered the wrong substance, witness wasn't co-signed, expired DEA license caught downstream → run a focused internal audit on the affected control area.
- Onboarding new staff — internal audit checklists double as training material; conduct one with the new hire shadowing.
- Pre-DEA-visit dry run — when you've been notified of an upcoming DEA inspection, run the DEA Compliance Pre-Audit template (or build a custom one mirroring the inspector's likely checklist). For a more focused readiness review, see DEA audit prep (deferred guide).
- State board pre-inspection — California's BAH inspection, for example, follows a specific 7-area checklist; build a CA-specific template to match.
Unlike the biennial (which inventories every container) or the spot check (which random-samples physical counts), the internal audit is process-focused: did we follow the policy? Are records complete? Are signatures present where required? Are licenses current? It's the systemic check, not the substance-level one.
Walkthrough
The internal audit feature has four pages organized as a flow:
/audits/internal → Hub page (cards link to Templates / Scheduled / History)
/audits/internal/templates → List + create/clone custom templates
/audits/internal/scheduled → Upcoming + in-progress audits
/audits/internal/history → Completed (and finalized) audits
/audits/internal/[id] → Audit detail (read-only after finalization)
/audits/internal/[id]/conduct → Conduct (in-progress) formStep 1 — Open the internal audits hub
Navigate to /home/<your-clinic>/audits/internal. The hub shows three cards: Templates, Scheduled Audits, Audit History, plus a Getting Started section for first-time users.

Step 2 — Pick or build a template
Click Templates. The list page shows all default + custom templates. Defaults ship with the system and cover common scenarios:
- Quarterly Compliance Review — DEA recordkeeping + security + license tracking
- DEA Compliance Pre-Audit — 8-category readiness review modeled on a real DEA inspection
- State Board Inspection Prep — generic state-board template (can be cloned and customized per state)
Click Clone on a default to create a clinic-custom variant you can edit, or Create Template to start from scratch.
A template is a structured checklist:
- Template name + description + category (compliance / security / record-keeping / quality / other)
- Items — each item has:
- Category (e.g., "DEA Registration", "Storage Security", "Record-Keeping")
- Description (the question/check, e.g., "DEA registration is current and posted in the controlled-substance area")
- Type —
boolean(pass/fail),text(free response),numeric(count or percentage),photo(require photo evidence) - Mandatory flag (cannot complete the audit without this item)
- Photo required flag (must attach evidence photo)
- Help text (per-item context shown to the conductor)
- Order (display sequence within the template)
Step 3 — Schedule the audit
Click Scheduled Audits → Schedule New Audit (or click the Schedule button on the hub page).
Schedule fields:
- Template — pick the template that matches the audit's purpose
- Audit date — when the audit will be conducted
- Assigned to — the staff member responsible for conducting (defaults to you)
- Due date — soft deadline (audit is
in_progressuntil completed; if pastdue_dateit's flagged as overdue but doesn't block) - Notes — optional context
Click Schedule Audit. The audit row appears in Scheduled Audits with status in_progress.
A screenshot of the scheduled-audits list view will be added here.
Step 4 — Conduct the audit
Click an audit row → Conduct, or navigate to /audits/internal/<auditId>/conduct. The conduct form renders each checklist item grouped by category:
For each item:
- Mark the result: Pass / Fail / N/A
- Add notes (always optional, but encouraged for
FailandN/A) - Attach a photo if
photo_requiredis set - For
textandnumericitems, fill the response field
Items with requires_corrective_action=true (typically Fail with severity) auto-flag for CAPA creation when the audit is submitted.
A screenshot of the conduct form will be added here.
Click Submit Audit when every mandatory item is recorded. The action:
- Upserts every item response into
audit_items - Calculates the overall compliance score as
(passing items / total scored items) * 100 - Updates the audit row to
status='completed', setscompleted_atandcompliance_score - Optionally creates linked CAPAs for items with
requires_corrective_action
Step 5 — Review and finalize
Navigate to /audits/internal/<auditId> (the audit detail page). The Compliance Scorecard card at the top shows:
- Overall compliance score (0-100%)
- Per-category scores (which categories are weakest)
- Items requiring corrective action (CAPA queue link)
The Audit Details card below lists every item with its result, notes, and any photos.

When the owner clicks Finalize Audit, a confirmation dialog explains that finalizing locks the audit permanently:

If the audit looks complete and accurate, click Finalize Audit (top-right of the detail view, owner-only). This is the irreversible step:
- The audit row is updated with
finalized_at = now()andfinalized_by = your_user_id - The audit's RLS policy now blocks any UPDATE on this audit (it's read-only forever)
- Linked CAPA items lock their immutable-linkage fields (audit_id, title, description, category, severity, assigned_by, assigned_date, created_at) — only lifecycle fields (status, completion notes, verification) remain mutable
- The Finalize button disappears from the UI; the audit is in the finalized state

Only owners can click Finalize Audit. Members (with audits.conduct permission) can complete the audit (status → completed) but cannot finalize it. This separation enforces a two-person review: the conductor records, the owner approves and locks.
Step 6 — Address corrective actions
After finalization, navigate to Discrepancies & Corrective Actions (/audits/internal/discrepancies) to see all CAPAs auto-created from this audit. Each CAPA links back to the source audit; you can also open CAPAs directly from the audit detail page.
Track each CAPA through its lifecycle:
open→ assigned, not yet startedin_progress→ work underwaycompleted→ remediation done; awaiting verificationverified→ verifier confirmed remediation is effective (closes the CAPA)
Verifiable CAPAs are the difference between "we wrote it down" and "we actually fixed it."
Field reference
Schedule audit form
| Field | Label | Type | Required | Validation |
|---|---|---|---|---|
checklist_template_id |
Template | select | ✓ | UUID; must be a template owned by this account or a system default |
audit_date |
Audit date | date | ✓ | ISO date; can be past, present, or future |
assigned_to |
Assigned to | select | optional | UUID of a clinic member; defaults to current user |
due_date |
Due date | date | optional | ISO date; soft deadline only |
notes |
Notes | textarea | optional | unbounded |
Audit submission (SubmitInternalAuditSchema)
| Field | Description |
|---|---|
audit_id |
UUID of the audit being submitted |
responses[] |
Array of { audit_item_id, checklist_item_id, item_result, item_notes, photo_url, requires_corrective_action } |
responses[].item_result |
pass / fail / na |
responses[].requires_corrective_action |
If true, a CAPA is auto-created on submission |
notes |
Audit-level summary notes |
Resulting database rows
| Table | Status transitions |
|---|---|
audits |
in_progress (created at schedule) → completed (at submit; completed_at + compliance_score set) → finalized (at finalize click; finalized_at + finalized_by set) |
audit_items |
one row per checklist item; upserted at submit |
corrective_actions |
one row per requires_corrective_action=true item; auto-created at submit; linked back via audit_id FK |
The Finalize Audit flow (owner-only)
The Sprint B audits-finalize-lock migration (2026-05-06) added the locking mechanism. The flow is:
- Owner views a completed audit —
audits.status = 'completed'ANDaudits.finalized_at IS NULL - Owner clicks Finalize Audit — owner-only; renders only when the user has
audits.approvepermission AND the audit is in the right state - Server action calls
finalize_audit(audit_id uuid)RPC — SECURITY DEFINER, gated onaudits.approvepermission - RPC sets
audits.finalized_at = now(),audits.finalized_by = auth.uid()— and returns true; idempotent (returns false if already finalized) - RLS now blocks UPDATEs on this audit —
audits_updatepolicy gated onfinalized_at IS NULL audit_itemsand linkedcorrective_actionslock:audit_itemsINSERT is blocked (the audit's items are immutable post-finalization)corrective_actionsINSERT is blocked (no new CAPAs can be linked to a finalized audit)corrective_actionsUPDATE is blocked for immutable-linkage fields only (audit_id, title, description, category, severity, assigned_by, assigned_date, created_at) via theprevent_finalized_capa_rewriteBEFORE UPDATE trigger; lifecycle fields stay mutable
The locking is enforced at the database layer (RLS + trigger), not just the UI. A direct API call to UPDATE a finalized audit will fail with a 42501 PostgreSQL error.
Edge cases
Mandatory items missing
The conduct form blocks submission if any is_mandatory=true item lacks a result. The error is inline; the user must fill or na the missing items.
Photo required but no photo
Items with photo_required=true must have a photo URL on submission; the conduct form's submit button blocks otherwise. There's no way to bypass this in the UI.
Compliance score interpretation
The score is (pass_count) / (pass_count + fail_count) * 100. na items don't count in either direction (they're scoped out). A 100% score means every scored item passed — but it doesn't mean every item was evaluated; if many items are na, the score may be deceiving. Always cross-reference the per-category breakdown.
Soft warning: low compliance score on finalize
If the audit's compliance score is below 80% at the moment Finalize is clicked, CS Logbook surfaces a soft amber warning ("This audit has a compliance score below 80%. Consider addressing flagged items before finalizing."). The owner can proceed — finalization is sometimes done specifically to lock in an audit that found significant problems, so the documentation is preserved.
Hard block: attempting to UPDATE a finalized audit via API
The RLS gate is hard-locked. A client.from('audits').update({...}).eq('id', finalized_audit_id) returns { data: null, error: null } with 0 rows affected — the policy silently filters. The UI doesn't expose this path on a finalized audit (every input is read-only), but the DB is the final defense.
Cancellation post-completion
If you completed an audit but realized it was conducted incorrectly (wrong template, wrong scope), open the audit detail page → Actions → Cancel. This requires a cancellation reason; the audit's status is set to cancelled and finalization is no longer available. CAPA items linked to a cancelled audit are kept (not soft-deleted) so the trail is preserved.
Re-running an audit on the same template
Nothing prevents scheduling a second audit using the same template — and you should, every quarter. Each scheduling creates a new audits row with its own id. The history page shows them ordered by audit_date.
Renaming / deleting a template that's been used in an audit
Templates linked to one or more audits cannot be deleted (DB error: "Cannot delete template - it is used by N audit(s)"). Templates can be updated in name/description/category but the items list cannot change once the template has been used (the linkage is preserved via checklist_template_id FK + immutable item snapshots in audit_items).
Common mistakes
- Building a template that's too long. A 50-item quarterly template is a chore; conductors will rush through it. Keep quarterly templates to ~20 mandatory items + ~10 optional.
- Marking everything
passwithout reading the items. The audit trail is the value; "everything passed" with no notes is suspicious to a real DEA inspector. - Not finalizing. An audit that's
completedbut neverfinalizedshows as un-locked in the history table — the audit-trail integrity is weaker. Finalize within a few days of completion. - Using one template for everything. Different audits have different scopes; build separate templates for quarterly review, DEA pre-audit, state-board prep, post-incident.
- Not creating CAPAs on
Failitems. The conduct form auto-creates CAPAs only whenrequires_corrective_action=trueis checked. If you mark items asFailwithout flagging the CAPA, the failure is recorded but the action plan isn't. - Letting CAPAs sit at
completedwithoutverified.Verifiedis the closing-the-loop step. An auditor reading "completed" sees a CAPA marked done but not double-checked.Verifiedis the difference.
Related
- Audits — overview — lifecycle, permissions, the Finalize step explained at the framework level
- Biennial inventory — the DEA's periodic required count
- Spot check — the monthly random-sample check that's lighter than an internal audit
- Sprint 17 internal audit feature plan — internal architecture reference
- ADR-006 enforcement philosophy — why the Finalize step is one of the few hard blocks