Portal Fixer

Portal Fixer setup guide

This guide is for a small B2B team with no HubSpot admin. It covers who the app is for, how you connect it, which permissions it asks for, and the Fix Catalog: the only fixes it can apply.

A dry run does not change the portal. Writes are off by default. Reversible description rewrites, custom-group moves, company-name backfills, and quarterly stage-probability updates are recorded as policy-approved for that change set under their playbooks. They do not wait for a separate approval.

Before you start

Connect the app

A Super Admin installs the Portal Fixer HubSpot app on the account. The app uses account-level OAuth. Portal Fixer does not create a user in the account.

Install Portal Fixer

The beta is invite-only. Installing does not keep the connection unless this HubSpot account is already on the beta list. Apply for the Fixer beta with the portal ID. Writes stay off. A dry run does not change the portal. Reversible description rewrites, custom-group moves, company-name backfills, and quarterly stage-probability updates are recorded as policy-approved for that change set under their playbooks. They do not wait for a separate approval.

  1. Apply, and wait until that portal is accepted.
  2. A Super Admin uses Install Portal Fixer above. The connection is the app itself, with account-level OAuth.
  3. Review the permissions below. You can leave an optional permission off. The fix that needs it does not run.
  4. Import the Portal Audit fix plan.
  5. Read the dry-run diff. Nothing is written during the dry run.
  6. Reversible description rewrites, custom-group moves, company-name backfills, and quarterly stage-probability updates are recorded as policy-approved and do not wait for a separate approval. Stage renames, adding or removing a stage, renaming a property group, creating or editing a workflow, and an owner assignment of more than 100 records to any one user wait for a person to approve that exact diff. See How it works.
  7. Apply writes only the approved fields, then reads them back. You can roll the change back from the snapshot.

Writes stay off until they are turned on for that portal. Apply does not change HubSpot while they are off.

Permissions the app asks for

The table is generated from the scope list in this repository.

ScopeWhere it is used
oauthInstall, token exchange, refresh, introspection
crm.objects.contacts.readbackfill:contacts.company search and read-back
crm.objects.contacts.writebackfill:contacts.company batch update
crm.objects.companies.readbackfill:contacts.company company names; owner rule company read
crm.objects.deals.readpipelines:template-stages; backfill:deals.hubspot_owner_id
crm.objects.owners.readBoth owner modules
crm.schemas.contacts.readdescriptions and groups, contacts
crm.schemas.contacts.writedescriptions and groups, contacts
crm.schemas.companies.readdescriptions and groups, companies
crm.schemas.companies.writedescriptions and groups, companies
crm.schemas.deals.readdescriptions, groups, and pipeline reads
crm.schemas.deals.writedescriptions, groups, and pipeline stage label PATCH
automation (optional)Workflow side-effect reads, assignment-workflow review, and the approval-gated workflows:assignment allowlist (POST/PUT /automation/v4/2027-03-beta/flows)
crm.objects.deals.write (optional)backfill:deals.hubspot_owner_id
crm.objects.tickets.read (optional)backfill:tickets.hubspot_owner_id
crm.objects.tickets.write (optional)backfill:tickets.hubspot_owner_id

Not requested yet

The uploaded HubSpot app grants the 16 scopes in the table. HubSpot's consent screen shows an unconfigured optional scope as disabled and leaves Connect disabled, so these stay out of optionalScopes until that app is uploaded with them:

ScopeWhere it will be used
crm.objects.companies.writebackfill:companies.hubspot_owner_id batch update. The module still dry-runs; apply refuses until the scope is granted.
crm.objects.line_items.read / crm.objects.line_items.writearchive:records: snapshot and archive one line item
crm.objects.quotes.read / crm.objects.quotes.writearchive:records: snapshot and archive one quote
crm.schemas.quotes.read / crm.schemas.quotes.writearchive:properties: snapshot and archive one custom quote property
crm.schemas.line_items.readLine item property settings. HubSpot has no line item schema write scope. No shipped module uses this scope.

archive:records and archive:properties stay in the registry. They stay off the public Fix Catalog until every scope they use is requested. Archive is the single-item DELETE on the archive allowlist in docs/safety.md; every other DELETE stays banned. Ticket properties cannot be archived: HubSpot lists no crm.schemas.tickets.* scope.

Not requested, and refused at startup if added: any *.sensitive.* or *.highly_sensitive.* scope, crm.export, crm.import, settings.users.write, settings.billing.write, forms, content, marketing-email.

Deal stage renames use crm.schemas.deals.write, not crm.objects.deals.write.

Optional rows are marked in the table. Without the automation permission, workflow side effects are not checked, and an assignment workflow is not created. Without deal write, deal owner fixes are not written. Without ticket read and write, ticket owner fixes are not written.

How to read a diff

Each row is one field. It shows the current value and the value that would be written. A note names a workflow when the automation permission was granted and an enrollment filter uses that property. If the permission was not granted, the note says workflow side effects were not checked.

Approve, apply, roll back

Reversible description rewrites, custom-group moves, company-name backfills, and quarterly stage-probability updates are recorded as policy-approved for that change set under their playbooks. They do not wait for a separate approval. These still wait for a person's approval of that exact diff: stage renames, adding or removing a stage, renaming a property group, creating or editing a workflow, and an owner assignment that gives more than 100 records to any one user. A run that would go beyond an item's limits does not happen automatically.

You can turn any fix type off for a portal. A type that is off does not run. Turning a type on does not write by itself.

Apply re-reads the portal. If a value changed after the diff was approved, the change set expires and nothing is written. When the diff still matches, Portal Fixer stores an encrypted snapshot, writes the allowlisted fields, and reads them back. You can roll back from the snapshot. Snapshots are kept 90 days by default.

Uninstall

Uninstall the Portal Fixer app in the HubSpot account. That stops Portal Fixer from accessing the account. Email jon@thejonmartin.com to delete the stored snapshots and tokens for that portal. That delete does not call HubSpot.

Fix Catalog

The catalog below is generated from the module list in this repository. It is the list of fixes Portal Fixer can apply. It describes what this app is built to change. It is not a claim that every API path below is confirmed on a current HubSpot page. Where it names a model, that step runs only when a provider is configured. The default asks a person or another program, and an unanswered step is skipped. Nothing is guessed.

The module key is the fix-plan item key. Items with any other key are report-only: the admin UI shows them and the runner will not dry-run them.

Playbooks in docs/playbooks/ decide approval. A dry run with rows and approval().required === false is recorded as policy-approved by policy. Apply is still a separate step and still passes every write gate. Empty diffs are not auto-approved.

ModuleApproval
descriptions:custom-propertiespolicy-approved when the rewrite is reversible
groups:custom-propertiespolicy-approved for moves; a groupLabel row requires a person
backfill:contacts.companypolicy-approved
pipelines:stage-probabilitiespolicy-approved inside the fiscal-quarter window
pipelines:template-stagesa person, for rename, add, or remove
backfill:deals.hubspot_owner_id and backfill:tickets.hubspot_owner_idpolicy-approved up to 100 records for any one user; above that, a person
workflows:assignmenta person, before any workflow write

The sample plan still maps six keys. pipelines:stage-probabilities and workflows:assignment run when a plan item uses those keys.

Stage structure changes use crm.schemas.deals.write. They do not use crm.objects.deals.write.

descriptions:custom-properties

groups:custom-properties

backfill:contacts.company

pipelines:template-stages

pipelines:stage-probabilities

Owners

workflows:assignment

Per-portal opt-out

portal_fix_types defaults to on when no row exists. A false row opts that fix type out for that portal. The global module flag remains an additional kill switch. Both must be on. WriteClient and the runner throw FIX_TYPE_DISABLED when the portal has opted out.