Procisely

PSI-owned URL shortener and asset link management platform. Provides persistent, branded short URLs that resolve to customer-facing destinations. Primary physical interface is a QR code sticker affixed to each shipped machine.


Overview

Procisely.com is a two-component platform:

  1. Redirect Engine — A public-facing Node.js service at procisely.com that resolves short URLs to destinations via 302 redirects. Customers scan QR code stickers on machines and are routed to the appropriate portal, documentation, or support page.

  2. Admin Interface — Integrated into the PSI Portal at portal.progressivesurface.com/procisely/. Provides redirect management, pattern rules, templates, QR code generation, bulk import, and analytics.

ResourceURL
Productionhttps://procisely.com
Admin UIhttps://portal.progressivesurface.com/procisely
Redirect Engine RepoProgressiveSurface/procisely-redirect
Admin CodePart of ProgressiveSurface/psi-portal
PRDC:\Users\AMD\OneDrive - Progressive Surface\Projects\URL Redirect\Procisely-PRD-V1.md

How It Works

Redirect Flow

Customer scans QR sticker on machine
  → procisely.com/S9820
  → Redirect engine looks up slug "s9820"
  → Match: SPN pattern rule (^S\d{4}$)
  → 302 → portal.progressivesurface.com/assets/s9820
  → Click event logged (slug, timestamp, user-agent, IP hash)

Resolution Precedence

  1. Exact slug match (Redirects table) — highest priority, allows per-slug overrides
  2. Pattern rules (PatternRules table) — regex-based, evaluated in priority order
  3. Default redirecthttps://www.progressivesurface.com

Rule Types

TypeWhen It RunsRequires DB Record?Example
Exact redirectRedirect timeYessupport → progressivesurface.com/support
Pattern ruleRedirect timeNo — matches via regex^S\d{4}$ → portal…/assets/{slug}
Rule templateCreation time onlyProduces recordsAdmin shortcut for batch creation

Architecture

Isolation Model

The redirect engine and admin interface are fully isolated. A compromise of the public service cannot reach internal resources.

LayerRedirect EngineAdmin (Portal)
ComputeDedicated App Service PlanExisting Portal App Service
NetworkPublic, no VNetPrivate endpoint
IdentityOwn managed identityPortal’s managed identity
SQL Roleprocisely_redirector (SELECT + INSERT only)procisely_admin (full CRUD)

Technology Stack

ComponentTechnology
Redirect EngineNode.js 22 LTS + TypeScript + Hono
Admin UIReact 19 + TypeScript + Vite + TailwindCSS (in Portal)
Admin APINode.js + Express (Portal backend routes)
DatabaseAzure SQL (Procisely on procserv-proddata)
QR Generationqrcode + sharp + archiver (server-side)
AuthEntra ID / MSAL (admin only)
HostingAzure App Service (Linux)

Infrastructure

Azure Resources

ResourceNameResource GroupDetails
App Service Planasp-procisely-redirectPS-WEBAPPSB1 Linux, dedicated (compute isolation)
App Serviceprocisely-redirectPS-WEBAPPSNode 22 LTS, Always On, public
Azure SQL DatabaseProciselyProcServices-Prod-DataBasic (5 DTU) on procserv-proddata
DNS Zoneprocisely.comPS-RG-01Azure DNS
SSL Certificateprocisely.comPS-WEBAPPSAzure-managed (GeoTrust), expires Oct 2026
Policy Exemptionprocisely-redirect-public-accessPS-WEBAPPSExempts redirect engine from “Deny App Service Public Network Access” policy
Application Insightspsi-webapps-insightsPS-WEBAPPSShared with other PSI web apps

App Settings (Redirect Engine)

SettingValue
SQL_SERVERprocserv-proddata.database.windows.net
SQL_DATABASEProcisely
SQL_AUTH_TYPEazure-active-directory-default
WEBSITES_PORT8080
NODE_ENVproduction
APPLICATIONINSIGHTS_CONNECTION_STRING(from psi-webapps-insights)

DNS Records (procisely.com)

RecordTypeValue
@A52.162.107.11 (App Service IP)
wwwCNAMEprocisely-redirect.azurewebsites.net
asuidTXTDomain verification hash

Domain registered at Network Solutions, nameservers delegated to Azure DNS (ns1-05.azure-dns.com through ns4-05.azure-dns.info).


Database Schema

Database: Procisely on procserv-proddata.database.windows.net

Tables

TablePurposeKey Columns
RedirectsSlug → destination mappingSlug (PK), DestinationUrl, RedirectType, Category, IsActive, HttpStatusCode
ClickEventsPer-click telemetryId (identity), Slug, ClickedAt, UserAgent, RemoteIpHash
RuleTemplatesCreation-time helpersTemplateId (PK), SlugPattern, DestinationPattern
PatternRulesRuntime regex rulesPatternRuleId (identity), Pattern, DestinationTemplate, Priority (unique)

SQL Roles

RolePermissionsUsed By
procisely_redirectorSELECT Redirects + PatternRules, INSERT ClickEventsRedirect engine managed identity
procisely_adminFull CRUD on all tablesPortal managed identity

Schema source: procisely-redirect/sql/schema.sql


Security

Redirect Engine Hardening

  • One route: GET /{slug} plus GET /health. No admin routes, no Swagger.
  • Slug sanitization: 5-step pipeline — URL decode, null byte rejection, CRLF rejection, alphanumeric+hyphen allowlist (max 128 chars), lowercase normalization.
  • Parameterized SQL queries only — slug never interpolated into query strings.
  • No user input in responses — returns only 302 + Location header. No HTML body, no error details.
  • Security headers on all responses: HSTS, CSP (default-src ‘none’), X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy no-referrer.
  • Rate limiting — in-memory per-IP rate limiter.
  • Click event IP privacy — SHA-256 hash only, full IP not retained.
  • Fire-and-forget click logging — click recording never blocks redirect response. Only logs clicks for actual matches (exact or pattern) — default fallback redirects (bot probes, unknown slugs) are not logged.

Pattern Rule Safety

  • Regex patterns are admin-authored only (never from public input).
  • Slug input bounded to 128 chars (limits ReDoS surface).
  • Invalid regex patterns are silently skipped at runtime.
  • Regex validation at creation time in the admin API.

Admin Features (Portal Integration)

All admin features are at portal.progressivesurface.com/procisely/*, behind Entra ID auth and private endpoint.

Pages

PageRoutePurpose
Dashboard/prociselyStats, default behavior config, active pattern rules, recent activity
Redirect List/procisely/redirectsFilterable table, inline toggle, bulk ops, CSV export
Redirect Editor/procisely/redirects/new or /:slug/editCreate/edit with template or custom mode
Bulk Import/procisely/bulk-importCSV upload, template import, combined onboard + QR flow
QR Generator/procisely/qrSingle/batch QR generation with logo overlay, plus a “From a pattern rule” panel — see below
Template Manager/procisely/templatesCRUD for creation-time templates
Pattern Rules/procisely/patternsRegex rule management with live tester
Analytics/procisely/analyticsClick trends, top redirects, category breakdown

QR Generator: printing pattern-rule assets

The generator lists redirect records. Pattern rules deliberately have none — so the assets they cover (usually the majority) were unprintable, and the workaround was to create records purely so the picker could see them, which inverts what pattern rules are for.

The “From a pattern rule” panel closes that: pick the rule, type or paste the values (newline / comma / tab separated, so a spreadsheet column pastes straight in), and every derived slug is validated against the rule’s real regex with its resolved destination shown. Values that wouldn’t route are flagged and excluded rather than silently printed — a sticker that dead-ends gets discovered by someone standing at a broken machine.

A regex can’t be reversed in general, so slug derivation is best-effort and validation is the guarantee.

Print stickers builds the sheet directly: four labels per Letter page, each carrying the machine’s name and number from TAG (GET /api/procisely/assets) so captions match the system of record. The bold line is editable and defaults to “Scan to report a problem”.

The TAG lookup is read-only by constructionlistEquipment is the only action and confirmExecution is never sent, so it cannot write. It caches for 5 minutes and falls back to the last good roster on failure, because a stale name on a label beats a failed print run. Without MOBI_API_KEY / MOBI_CONNECTOR_INSTANCE_ID the panel still works and prints the code instead of the name.

The 4up preset is 3.9 × 4.5in, and the width is load-bearing

With the default 0.25in margins and 0.1in gap, two columns need each label at or under 3.95in. At 4.0in the grid silently collapses to one column and prints two per page — no error, just wrong. computeGrid() is exported and tested against the real preset for exactly this reason.

API Endpoints

All endpoints at /api/procisely/*, authenticated via Entra ID bearer token.

MethodPathPurpose
GET/healthHealth check (no auth)
GET/POST/PUT/DELETE/redirects/*Redirect CRUD + bulk + from-template
GET/POST/PUT/DELETE/templates/*Template CRUD
GET/POST/PUT/DELETE/patterns/*Pattern rule CRUD + test + reorder
GET/analytics/*Click analytics + top redirects
GET/assetsTAG asset roster (read-only) for label captions
GET/redirects/:slug/qrSingle QR generation — accepts any slug; no database lookup
POST/redirects/batch-qrBatch QR ZIP download — likewise, arbitrary slugs
POST/redirects/labels-pdfPrint-ready QR label sheet (QR + readable text, N-up)
POST/redirects/bulk-onboardTemplate import + QR download in one flow

Seed Data

Default Templates

TemplateSlug PatternDestinationCategory
Asset Customer Portal{serial}portal.progressivesurface.com/assets/{serial}asset
Recalibration Requestrecal-{serial}portal.progressivesurface.com/recalibration?asset={serial}support
Documentation Librarydocs-{serial}docs.progressivesurface.com/machines/{serial}asset
Campaign Landing Page{auto}(user-specified)campaign
Internal Tool Link{slug}(user-specified)internal

Default Pattern Rule

NamePatternDestinationPriority
SPN Asset Portal^S\d{4}$portal.progressivesurface.com/assets/{slug}10

This rule means every PSI serial number (S + 4 digits) automatically redirects to the asset portal without creating individual redirect records.


CI/CD

Redirect Engine

SettingValue
TriggerPush to main
Runnerpsi-internal (ps-cicd-runner, Ubuntu)
Authaz login --identity (managed identity)
Deployaz webapp deployment source config-zip
Tests70 unit + integration tests (Vitest)

Portal (Admin UI + API)

Deployed as part of the standard Portal CI/CD pipeline. Push to master triggers build + deploy.

Additional tests: 30 API route tests (Vitest + Supertest) via npm run test:api.


Testing

SuiteCountFrameworkCommand
Redirect engine unit tests62Vitestcd procisely-redirect && npm test
Redirect engine integration8Vitest + Hono app.request()Same
Portal API route tests30Vitest + Supertestcd psi-portal && npm run test:api
Total100

QR Code Specifications

SettingValue
Contenthttps://procisely.com/{slug}
FormatsPNG (raster) and SVG (vector)
Sizes128px to 2048px (PNG), scalable (SVG)
Error correctionLevel M (15%) default, Level H (30%) with logo
Logo overlayWhite rounded knockout plate with the PSI mark on it, ~36% of the QR width and proportioned to the mark. Covers ~8% of the code’s area against Level H’s ~30% budget.
Print quality300 DPI minimum for 1” stickers
Batch exportZIP archive with filenames matching slugs, or a 4up label sheet (four per Letter page)

The logo used to be composited straight onto the code

It was chroma-keyed to transparency so only the letterforms obscured modules — “no rectangular halo”. The result read as a logo lost in noise. It now clears a quiet area, which is what every polished QR generator does, and Level H error correction pays for it.

The scannability claim is tested, not assumed. tests/server/procisely/qrDecode.test.js decodes the generated image with jsqr and asserts the URL survives — with and without the logo, at 256/512/1024, and with a long slug where the code is densest and the plate covers proportionally more modules. A QR that looks right and doesn’t scan fails silently, in a plant, in the hands of someone trying to report a fault.

If you raise PLATE_RATIO for looks, those tests are what stop you shipping an unscannable sticker.


Public QR endpoint — GET /qr/:slug

A QR code you can reach by URL, served by the public redirect engine. No authentication, no portal, no download step.

https://procisely.com/qr/EQ-1168              PNG
https://procisely.com/qr/EQ-1168?size=1024    larger (capped at QR_MAX_SIZE)
https://procisely.com/qr/EQ-1168?format=svg   vector, for real label stock

Printable labels

?top= and ?bottom= wrap the code in a label with text above and below — a bare QR on a machine tells nobody what it’s for:

https://procisely.com/qr/EQ-1168
  ?top=Scan+to+report+a+problem
  &bottom=CNC+Waterjet+%C2%B7+1168

Captions are composed as SVG and therefore force SVG output even if format=png was requested — returning a PNG silently missing the requested text would be worse than ignoring the format hint. Captions are XML-escaped and capped at 60 characters.

Why this exists separately from the portal generator

The portal’s QR page is Entra-authenticated behind a private endpoint, so its images can’t go in an <img> tag, a Word document, a label template, or an asset record in another system. This endpoint can — it is the primitive; the portal page is the convenience layer.

In practice: TAG asset records attach this URL directly (TAG attachments support isUrl), so the sticker artwork lives on the asset and always renders current. Nothing to re-upload when a caption changes.

Behaviour worth knowing

BehaviourWhy
The slug is not resolved firstPattern rules match at request time and have no records. Requiring a code to “exist” would make exactly the assets that most need printing unprintable.
qr is a reserved prefixRegistered before the catch-all /:slug, which would otherwise match /qr/EQ-1168 as slug qr. No redirect may use qr as its slug.
Size is cappedPublic and unauthenticated — an uncapped size is CPU amplification. Per-IP rate limiting applies on top.
Cached 30 daysA code for a slug is deterministic, so repeat requests are free.
Encodes QR_BASE_URL, not the request hostA code minted via the azurewebsites.net hostname must still point at procisely.com.
No click is loggedPrinting a label is not a scan; counting it would inflate the analytics that show whether stickers are actually used.
No logo overlayThat needs sharp, a heavy native dependency, on a small internet-facing service. Logo’d codes stay in the portal.

Future Roadmap

FeaturePriorityTrigger
Business Central integrationHighBC go-live — auto-create asset links from shipped machines
Geolocation analyticsMediumWhen click volume justifies GeoIP cost
Dynamic customer routingMediumPortal queries customer from serial
Bot detectionLowClick analytics reliability
Link expirationLowCampaign use cases grow
CPQ integrationMediumAuto-generate links at order time
Per-redirect 301 optionLowSEO benefit for permanent redirects (schema field exists)