Conversation
Documents the full public-ticket surface that shipped across all 10 host-framework plugins in this rollout: - Three guest-policy modes (unassigned / guest_user / prompt_signup) with the decision table - Admin settings page at Admin → Settings → Public Tickets backed by GET/PUT /escalated/admin/settings/public-tickets endpoints - Runtime validation semantics (unknown-mode coercion, mode-switch cleanup, 500-char template truncation, zero-user-id handling) - Widget submission flow + per-email rate limit - Inbound email 5-priority ticket-resolution chain (In-Reply-To → References → signed Reply-To → subject reference → legacy lookup) - Workflow integration pattern (ticket.created fires against public submissions too) - promoteToUser back-stamp for signup invite flow - Contact-pattern data model (escalated_contacts + nullable contact_id on escalated_tickets) Wires the new page into docs.json between Tickets and Bulk Actions so the sidebar reflects the natural reading order.
The doc claimed two {{...}} placeholders ({{email}} and {{token}}),
but the actual NestJS implementation
(src/services/email/email.service.ts:157) only substitutes a single
{token} placeholder (single braces) with an HMAC-scoped one-time
token derived from the contact id. No {email} substitution exists.
Correct the syntax and drop the {email} placeholder that never
existed. Also explain how the token is used for verification on the
host-app side.
The shipped PublicSubmitThrottleGuard keeps hits in a plain Map. Fine for single-instance + tests, but a multi-instance deployment needs Redis or equivalent to enforce the limit globally. Flag that in the widget section so host maintainers don't discover it the hard way in production.
The previous snippet invented a window.escalated('init', { tenantId })
pattern that doesn't exist. The real widget loader
(src/widget/index.js in the escalated repo) reads config from
data-base-url / data-color / data-position attributes on the script
tag (or from a pre-set window.EscalatedWidget object) and mounts into
a shadow-DOM host. No tenant concept exists.
Also noted that the widget fetches /widget/config on load to pick up
admin-configured branding overrides, so the snippet's data-color is
just a boot-time default.
The InboundRouterService resolves in exactly 4 priorities (In-Reply-To, References, signed Reply-To envelope, subject reference). I invented a 5th 'legacy InboundEmail audit lookup' priority that doesn't exist in the code. Dropped it and clarified that References walks the whole chain (the code loops), not just a second parse. Also tightened the fall-through line to mention the Contact resolve-or-create step, which is the real behavior of the fall-through branch (router.ts:101).
NestJS WidgetController has no GET /config route. The shared Vue widget calls /support/widget/config with a try/catch, falling back to data-* attributes when the endpoint is absent — so Laravel/Rails/ Django (which do serve /config) get dynamic admin branding, and NestJS stays on the boot-time data-color. Also corrected the tickets endpoint path — /escalated/widget/tickets is NestJS-specific; other frameworks mount under /support/widget.
The intro and Inbound Email section both hardcoded
/escalated/webhook/email/inbound as THE endpoint. That's NestJS-only.
Laravel exposes /support/inbound/{adapter} (verified in
routes/inbound.php), Django has widget/config but a different inbound
path, Adonis has its own controller, etc. Route the reader to the
framework-specific Inbound Email page for the exact URL shape, and
show NestJS + Laravel examples inline so they have a concrete
starting point.
…tions Claimed all 11 plugins support Postmark, Mailgun, and SES. In reality only the NestJS reference plus the five greenfield plugins have all three — the five legacy host-adapter plugins (Laravel, Rails, Django, Adonis, WordPress) ship Postmark + Mailgun only, no SES. Verified by filesystem grep across each plugin's mail/ adapters directory. Replaced the flat sentence with a table so readers don't implement against SES on a framework that doesn't serve it.
The JSON snippet flattened guest_user_id + signup_url_template into
one object with null defaults. The actual type (verified against
EscalatedModuleOptions in escalated-nestjs/src/config/escalated.config.ts:75)
is a tagged union — { mode: 'unassigned' } | { mode: 'guest_user',
guestUserId: number } | { mode: 'prompt_signup',
signupUrlTemplate?: string } — where mode-specific fields only appear
on the matching variant. Showed all three forms so readers don't
construct an invalid mixed object.
Also noted the camelCase-vs-snake_case split: NestJS/.NET/Go use
camelCase for the boot option; Laravel/Rails/Django/Symfony use
snake_case. The runtime settings API (below) always round-trips
snake_case regardless of framework.
The previous line said the unique email index on escalated_contacts.id was 'case-insensitive'. Two mistakes: 1. The index is on the email column, not id (id is the integer PK). 2. Case-insensitivity is enforced at the SERVICE layer (ContactService.findOrCreateByEmail lowercases before query/insert, verified at src/services/contact.service.ts:16-18), not at the DB index level. The SQL index itself is case-sensitive like any standard unique index. Rewrote as two bullets — one about the contact table's unique email index + service-layer normalization, one about the ticket.contact_id FK — so the index location and the case-insensitive behavior both land in the right mental place.
… endpoint
The runtime-switching section showed GET + PUT /escalated/admin/settings/public-tickets
as THE way to update guest policy. That's wrong for the NestJS
reference — NestJS's AdminSettingsController only has a generic
PUT /escalated/admin/settings that takes [{ key, value, type, group }],
and the widget controller reads a single 'guest_policy' key containing
a JSON blob.
The dedicated /settings/public-tickets endpoints exist on all 10 host-
framework plugins (Laravel/Rails/Django/Adonis/WordPress/Symfony/.NET/
Go/Spring/Phoenix) but NOT on the NestJS reference.
Also documented the example URL as /app.example.com/signup?from_ticket={token}
instead of the invented ?email={{email}} syntax — consistent with the
Template variable in signup URL fix from an earlier iteration.
Open
3 tasks
Removes the 'NestJS reference: no dedicated endpoint' caveat now that escalated-nestjs#27 lands GET+PUT /escalated/admin/settings/public-tickets matching the 10 host-framework plugins. Switched the table from a 2-row framework-vs-NestJS split to a per- framework prefix lookup so readers get the exact URL for their plugin. All 11 frameworks now share the same snake_case wire format at the same relative path.
Open
4 tasks
The previous line said the widget 'resolves a Contact for existing host-app users AND fills requester_id with their host-app user id, so both paths work'. That's wrong. Reading widget.controller.ts createTicket: - If body.email is supplied: resolves/creates a Contact, sets requester_id from the CURRENT GUEST POLICY (not the user's host-app id — the submission is treated as a guest submission). - If body.requesterId is supplied instead: sets requester_id to the supplied value, no Contact is resolved. These are two separate mutually-exclusive paths. Rewrote to describe them as such, with the note that one-or-the-other is required (400 otherwise) and email is the recommended path for new integrations.
The data-model section wrote column names as escalated_tickets.contact_id and escalated_contacts.user_id. Those snake_case names are right for Laravel/Rails/Django/Symfony/WordPress but wrong for NestJS/.NET/Go where TypeORM's default uses the property name exactly (camelCase) and no snake-casing strategy is configured. Fix by showing both forms and noting the per-ecosystem convention. Verified against src/entities/contact.entity.ts — no NamingStrategy configured anywhere in the project, so @column properties literally map to same-name columns.
2 tasks
The shared Vue widget hardcoded /support/widget and couldn't reach NestJS's /escalated/widget routes. Fixed in escalated#35 by adding a data-widget-path override. Document it here so NestJS users know to set the attribute. Also switched the 'On open' paragraph from hardcoded paths to a template (<widget-path>/config, <widget-path>/tickets) since the behavior is now path-parametric.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds `sections/public-tickets.md` covering the feature that shipped across every host-framework plugin during the public-ticket rollout — until now it only had scattered mentions in the NestJS CHANGELOG and the per-framework inbound-email pages. Public Tickets is arguably one of the bigger user-visible features in the plugin set and deserves its own docs page.
What it covers
Wires the new page into `docs.json` between Tickets and Bulk Actions so the sidebar follows the natural reading order.
Test plan