Licenses, Permits and Charitable Registration
Track licenses and professional permits with a responsible owner and a five-tier alert cadence, record the state charitable registrations you need before you ask people in that state for donations, and act on regulatory alerts.
The Licenses module is Muin’s register of business licenses, professional permits, regulatory registrations, and certifications. Each license has a responsible owner, alert cadence tiers, renewal history, a linked calendar event, and an OCR pipeline that extracts the expiry date from the uploaded certificate so nobody has to type it twice.
If a license lapses because nobody was watching the calendar: the whole organization can be operating illegally for days, weeks, or months. This module exists so that doesn’t happen.
When you’d use this
- A seller’s permit, professional license or certification is coming up for renewal and you want the alert to find you rather than the other way round.
- You solicit donations in more than one state and need to know which registrations you have on file.
- A regulator published a change and you need to record a response plan or state why it does not apply to you.
- An auditor asks for the current certificate and you want to hand over the file, not a promise.
The Licenses List
On /compliance/licenses, each row shows:
| Column | What it shows |
|---|---|
| Name | Business-facing name (e.g. “Seller’s Permit”) |
| License number | Issuing agency’s reference |
| Status | active, expiring_soon, expired, suspended, revoked, pending |
| Issuing authority | State / city / professional board |
| Expiration date | Next expiry |
| Days to expiry | Live countdown, colored by tier |
| Responsible user | Accountable owner |
Filters let you narrow by status, authority, category, or the assigned owner. The default filter excludes expired licenses that have been superseded by a renewal: see the renewal-filter note in the FAQs.
Creating a License
Click New License. The form asks for:
| Field | Purpose |
|---|---|
| Name | Friendly descriptor |
| License number | Issuing agency’s number |
| License Type | Business license, professional license, the operating, health, fire, zoning and environmental permits, import/export, tax registration, industry certification, other, and the filings the Compliance Calendar loads: Annual Report and Tax Filing |
| Issuing authority | The regulator or agency |
| Issue date | When first issued |
| Effective date | When this version takes effect |
| Expiration date | Next expiry |
| Alert days | Days before expiration to fire alerts: defaults to [90, 60, 30, 14, 7] |
| Responsible user | Who gets the alerts |
| Annual fee | Monetary cost for reporting |
| License document | The PDF / image scan of the certificate |
| Renewal lead days | How many days before expiration the renewal workflow unlocks |
When you edit a license, Needs this filed first links it to another filing it waits on, such as a renewal that needs a tax clearance. While that filing is overdue, the Compliance Calendar will not mark the renewal filed.
The alert_days field is a JSONB list on the license model (ExpirationMixin.alert_days): per-license override is allowed if a particular jurisdiction wants a different cadence. The default matches the standard “90-60-30-14-7 urgency ramp” that works across most regulatory bodies.
The Five Alert Tiers (R11-EXPIRATION-WORKER)
A daily worker at 08:00 UTC walks every active license. For every tier in alert_days that the license’s time-to-expiry hits today, the worker:
- Fires a notification to the
responsible_userviaNotificationDispatcher - Creates a
HumanTask(category=LICENSE_RENEWAL)pointing to the license detail page - Stamps
license_alert_sent[tier]with the send timestamp (a JSONB@>containment check) so re-runs of the worker on the same day are no-ops
Idempotency
The license_alert_sent JSONB map is the worker’s idempotency ledger. It keys by tier:
{
"90": "2026-01-15T08:00:00Z",
"60": "2026-02-14T08:00:00Z",
"30": "2026-03-16T08:00:00Z"
}
If the worker retries (e.g. after a transient failure), it sees the tier already stamped and skips the dispatch. Editing the alert_days list afterwards doesn’t reset the map, so adding a new 45 tier to a license that’s already past the 45-day mark doesn’t trigger a missed-alert blast.
When the Map Is Reset
The map is reset to {} on renewal: the license’s next cycle starts fresh, so the new 90-day mark will fire even if the previous cycle’s 90-day mark was already stamped.
Priority Mapping
| Tier (days to expiry) | HumanTask priority |
|---|---|
| 7 | CRITICAL |
| 14 | HIGH |
| 30 | MEDIUM |
| 60 | MEDIUM |
| 90 | LOW |
Tuned so the 7-day-out alert pages the responsible user through whatever high-priority routing the tenant has configured (escalation emails, Slack @channel, etc.).
The Renewal Wizard (IS19)
Click Renew on a license within its renewal window. Muin opens a four-step wizard:
Step 1 · Pre-Fill
The wizard opens with the current license fields pre-populated. Review and adjust:
- License number (if the agency assigns a new one on renewal)
- Effective date (first day of the new period)
- Expiration date (placeholder: gets overwritten after Step 3)
- Annual fee amount
- Notes
Step 2 · Receipt Upload
Upload the fee receipt from the issuing agency. This becomes part of the renewal history record. The upload goes through the standard Documents module pipeline (virus scan, versioning, retention tagging).
Step 3 · Certificate Upload + OCR
Upload the new license certificate (the renewed PDF / image from the agency). Muin’s Document Intelligence pipeline routes it to the license_cert extractor which pulls:
- License number (verified against the pre-filled value; warns on mismatch)
- Effective date
- Expiration date: populates the form’s expiration field
The OCR suggestion is editable, if the extractor misreads a date (handwritten certificates, low-quality scans), correct it before submit.
Step 4 · Submit
Review the final values and click Confirm Renewal. The service performs an atomic update:
- Records the renewal in
renewal_historywith previous + new expiration + fee + notes - Updates effective and expiration dates
- Resets
license_alert_sentto{} - Clears the old calendar event reference so the next worker pass creates a fresh one
- Bumps
version(optimistic lock counter) - Emits a
compliance.license.renewedlog event
Optimistic Locking (G-Licenses-5)
The atomic update is gated on version = :expected_version and bumps the counter in the same statement. If two owners click Renew simultaneously, only the first commit wins. The loser either:
- Re-reads and returns idempotent success if its payload matches what’s now committed (same effective_date, expiration_date, fee)
- Raises
409 LicenseRenewalConflictErrorif the payloads diverge
This keeps renewals race-safe without needing global locks.
Renewal Window Gate (G-Licenses-2)
You can’t renew a license that isn’t within its renewal window. The service raises:
License 'Seller's Permit' is not within the renewal window of
90 days (expires 2026-07-15). Renewal is only allowed from 90 days before
expiration.
This prevents accidentally renewing a license 6 months early because someone clicked the wrong row.
Expired licenses can still be renewed: the window rejection only blocks premature renewals.
SOC 2 Evidence Auto-Update (JG-9)
When a license is renewed, the compliance.license.renewed event is consumed by the SOC Audit subscriber. If the license is linked to a framework control (typically via the compliance module mapping), the subscriber:
- Updates the evidence freshness date for the linked control
- Attaches the new license document as current evidence
- Marks the previous license document as archived evidence (retained for audit)
This means the auditor’s evidence-collection view stays current automatically, no manual “I renewed the license, now I need to remember to update the SOC 2 control” step.
Linking is configured on the license detail page under Linked Controls.
Calendar Integration
Each active license has a linked calendar event (calendar_event_id) representing the expiration date. The expiration worker regenerates this event on renewal so the calendar always shows the current cycle. Events appear on:
- The responsible user’s personal calendar (via the tenant’s Calendar integration)
- The tenant-wide Compliance Calendar at
/compliance/calendar(if enabled)
Deleting a license removes its calendar event; renewing it moves the event to the new expiration date.
Charitable Registration
/compliance/charitable-registration answers one question per entity per state: what does that state’s statute say about this entity, and do you have a registration on file for it?
- Go to Sidebar → Compliance → Obligations → Charitable Registration.
- Each row pairs one of your filing entities with one state, and names the regime it falls under, Soliciting contributions or State tax exemption, plus the authority and a plain-language explanation.
- The page splits into No registration on file and Everything else, so the gap is at the top.
- Each row carries a posture badge: On file, Nothing to file, No registration on file or Not assessed.
- Click Record registration on a row (or Edit where a license already exists) and fill in the modal:
| Field | What to enter |
|---|---|
| What to call this registration | Required. Save stays disabled until it is filled in. |
| What is on file | Registered with the state · Exemption granted by the state · Exemption applied for, not yet granted · Exempt by definition, nothing to file · Not applicable to this entity |
| Registration number | The state’s reference. |
| Effective date / Renews on | The dates the state gave you. |
| Notes | Anything the next person needs. |
- Click Save. The registration is stored as a license, so it picks up the same alert cadence and renewal history as everything else on this page.
The page states its own scope: “States covered here: …”. A state that is not listed has not been assessed, which is not the same as nothing being required there.
Entities come first. This page assesses the entities you recorded under Filing Entities. With none recorded it reads “No filing entities recorded yet”, and the fix is to record what each entity is, not to add a registration here.
Regulatory Alerts
/compliance/regulatory is the feed of published regulatory changes, with an audit trail of what you did about each one.
- Go to Sidebar → Compliance → Programs → Regulatory Alerts.
- Narrow the feed with the three filters: category (Employment, Tax, Safety), priority (Critical, High, Medium, Low) and jurisdiction (a state, Federal, or all).
- A banner at the top counts what needs attention: “N critical, N high-priority alert(s) require attention”.
- Each alert carries an impact category, Immediate action, Review needed, Informational or No action, the effective date, the jurisdiction and a link to the regulator’s own source.
- Act on an alert:
- Summarize impact asks Muin to explain what the change means for you.
- Create response plan turns the alert into a plan you can work.
- Dismiss opens “Dismiss regulatory alert”. Record why it does not apply, for example “Does not apply: we have no operations in this jurisdiction.” The rationale is retained for audit.
What it affects
- A license renewal refreshes the linked SOC 2 control evidence and moves the calendar event (see the sections above).
- A charitable registration recorded here appears as a license, so it joins the same expiry alert tiers and the compliance score.
- A dismissed regulatory alert keeps its rationale on the record; it is not deleted.
- Charitable Registration and the Filing Calendar read the same filing entities, so adding an entity changes both.
When it goes wrong
| What you see | What it means | How to clear it |
|---|---|---|
| “Could not load charitable registrations” with a server message | The compliance service did not answer. | Click Try again. |
| “The registration could not be saved. Check the fields and try again.” | A field was rejected. | The usual cause is a blank name. Fill in What to call this registration and save again. |
| A row with no Record registration button | The posture is Not assessed, so there is nothing to record against yet. | Record the entity’s filing position first under Filing Entities. |
| “Nothing outstanding in the states we model” | Every entity either has a registration on file or is outside the statute in each state listed. | Nothing to do. Read the scope line to see which states those are. |
| “No regulatory alerts yet” | No alerts have been published to your jurisdictions yet. | Nothing to do: new alerts appear as regulators publish them. |
| “No alerts match your filters” | Your three filters exclude everything. | Clear the category, priority and jurisdiction filters. |
| “Couldn’t reach the AI service. Try again in a moment.” | Summarize impact or Create response plan could not run. | Try again. Nothing was recorded against the alert. |
Next step
- Filing Entities and the Filing Calendar: the entities this page assesses, and their statutory dates.
- Inspections: recurring facility tests rather than one-off permits.
FAQs
How do reminders work?
A daily worker fires at 08:00 UTC. For each license, it compares today to expiration_date - tier for every tier in alert_days (default [90, 60, 30, 14, 7]). If today matches any tier and that tier hasn’t already been stamped in license_alert_sent, the worker dispatches a notification + HumanTask to the responsible user. Priority ramps from LOW (90-day) through CRITICAL (7-day). Re-runs of the worker on the same day are idempotent via the JSONB containment check.
What if the OCR gets my expiry date wrong?
The extracted value is a suggestion: it populates the form, but the field is editable. Review before submit. If you renew a license with a wrong expiry date, the renewal is atomic (either fully applied or not at all) and the renewal_history retains the submitted value, so correcting it is a matter of entering a new renewal with the right date. Common OCR pitfalls: handwritten dates on old certificates, low-contrast scans, non-US date formats, adjust manually when those bite.
How do I renew manually?
If the renewal wizard doesn’t fit your flow (e.g. you renewed through the agency’s portal months ago and you’re just catching up Muin), you can update the license directly: edit the license, change effective + expiration dates, save. The license_alert_sent map clears on that update so the next cycle’s alerts fire clean. The renewal wizard is preferred because it captures receipt + OCR-verified expiry + SOC 2 auto-update, but manual updates are a supported fallback.
Why can’t I renew a license that expires in six months?
The renewal window guard (G-Licenses-2) rejects renewals outside renewal_lead_days before the expiration. The default is 90 days; you can raise it per-license on the edit form if your jurisdiction allows earlier filing. The rejection error message includes the exact window so you know when the renewal unlocks.
What happens to the alert map when I change alert_days?
Nothing: the map keys by tier value. Changing the tier list doesn’t reset the map. If you add a tier (say, 45), the worker will fire for 45 the next time a license’s time-to-expiry hits that value. If you remove a tier (say, removing 90), stamps for the removed tier are harmless leftovers: they don’t fire anything. Resets only happen on renewal.
Related
- Compliance Health: license compliance is 5% of the score
- Compliance Frameworks: link a license to a framework control
- SOC 2 audit: how license renewal auto-refreshes evidence (JG-9)
- Document assembly: the Documents pipeline that processes uploaded certificates