sails-hook-slipway
v0.0.13
Published
Connect Sails apps to Slipway with Lookout, Bridge support views, Wake analytics, release flags, and Bearing feedback.
Maintainers
Readme
sails-hook-slipway
The Sails hook that connects an application to Slipway.
It currently provides:
- automatic request, exception, Waterline, Quest, and cache telemetry for Lookout;
- app-scoped boolean release flags with deterministic rollouts;
- app-owned Bearing feedback, roadmap, updates, and an optional widget; and
- a secure app-local
/bridgeentry point for people who manage application data without receiving Slipway infrastructure access.
Installation
Requires Node.js 22 or newer.
npm install sails-hook-slipwaySlipway detects the hook and injects its deployment credentials automatically.
Do not copy Bridge or telemetry credentials into config/slipway.js.
App-local Bridge
Enable Bridge for an app from App → Bridge access in Slipway, then redeploy the app. Slipway injects a dedicated, app-scoped exchange credential into that deployment.
People can enter Bridge through either URL without changing origin mid-session:
https://your-app.example.com/bridge
https://slipway.example.com/projects/<project>/environments/<environment>/apps/<app>/bridgeThe public URL keeps navigation, search, actions, and assets under the app's
own /bridge path. The Slipway URL is the operator entry point. If the app is
mounted below a route prefix, that prefix comes before /bridge; a root app
uses exactly /bridge.
The hook uses the app's existing authenticated user as the identity. Slipway only activates an invitation when the signed-in user's verified email exactly matches the invited email. The account may be created after the invitation is sent, but it must exist and verify that address before Bridge opens. Bridge users are not added to the Slipway team.
The default Boring Stack conventions work without app code:
- model:
User - session key:
userId - email:
email - name:
fullName - verification:
emailStatusisverifiedorconfirmed
Persist that provider-neutral verification state when Wish, a redeemed magic
link, or another authentication flow proves ownership of the exact email
stored on the user. A provider that verifies a different candidate email must
not confirm the current address. Opening /bridge must never call GitHub,
Google, or another identity provider, and Bridge does not need stored OAuth
access tokens.
Declarative identity mapping
For conventional model-backed sessions with different names, map the attributes instead:
module.exports.slipway = {
bridge: {
loginPath: '/login',
identity: {
model: 'member',
sessionKey: 'memberId',
emailAttribute: 'emailAddress',
nameAttribute: 'name',
emailVerifiedAttribute: 'hasVerifiedEmail'
}
}
}Declarative host authorization
Bridge invitation roles are a ceiling. Narrow them with the host user's persisted role without an application authorization helper:
module.exports.slipway = {
bridge: {
authorization: {
roleAttribute: 'role',
roles: {
admin: ['*'],
editor: ['viewAny', 'view', 'create']
},
default: []
},
resources: {
purchase: {
authorization: {
roles: {
admin: ['viewAny', 'view'],
editor: []
}
}
}
}
}
}Slipway resolves the host user by the stable ID established during the Bridge exchange and reads the configured role once per authorization pass. Unknown users, roles, actions, and invalid configuration fail closed.
Direct domain actions
An action can invoke an explicitly allowlisted application domain helper with validated fields as named inputs:
issueLicense: {
scope: 'resource',
helper: {
identity: 'license.createLicense',
inputs: 'values',
result: {
message: 'License issued for {{email}}. Copy this key now: {{key}}'
}
},
fields: {
email: { type: 'email', required: true },
maxUses: { type: 'number', required: true, min: 1, max: 2 }
}
}Sails still validates the domain helper's declared inputs. The result template
is rendered inside the target app and only the bounded message crosses into a
one-time Bridge flash. Add context: ['actor', 'recordId'] only when the domain
helper explicitly declares those inputs. The browser can never choose a helper
identity.
Custom authentication escape hatch
Use an identity helper only when authentication is not model-backed:
// config/slipway.js
module.exports.slipway = {
bridge: {
loginPath: '/sign-in',
identity: {
helper: 'bridge.identity'
}
}
}// api/helpers/bridge/identity.js
module.exports = {
friendlyName: 'Resolve Bridge identity',
inputs: {
req: { type: 'ref', required: true }
},
exits: {
success: { outputType: 'ref' }
},
fn: async function ({ req }) {
const member = await Member.findOne({ id: req.session.memberId })
if (!member) return null
return {
id: member.id,
email: member.email,
fullName: member.name,
emailVerified: member.emailVerified === true
}
}
}The helper must return emailVerified: true. Bridge fails closed when it cannot
prove email verification.
Bearing
Bearing gives each deployed app feedback, roadmap, and update pages on the app's own domain. Enable it from App → Bearing, choose who may participate, then redeploy once so Slipway can inject the app-scoped exchange credential.
https://your-app.example.com/bearing/feedback
https://your-app.example.com/bearing/roadmap
https://your-app.example.com/bearing/updates/bearing redirects to /bearing/feedback. Keeping every public surface below
the Bearing namespace prevents Slipway from claiming app routes such as
/feedback, /roadmap, or /updates.
When the widget is enabled, the hook adds one same-origin, asynchronous bootstrap script to successful HTML responses. It does not edit the app's templates, weaken its Content Security Policy, block a request on Slipway, or render anything while the control plane says the widget is off. The capability document is refreshed in the background with release flags.
The widget says What's new only when a published update is newer than the last update that visitor opened. Opening it stores that update's public ID as a local seen watermark. Its quiet lower-corner trigger opens a compact panel and gets out of the way while that panel is open. Escape, an outside click, or the panel's close control dismisses it and returns focus. The trigger stays hidden until another update is published. No published or unseen update means no injected UI is visible.
The host app can open the same panel on any surface from explicit actions in a menu or toolbar:
<button type="button" data-slipway-bearing-open="feedback">
Share feedback
</button>
<button type="button" data-slipway-bearing-open="roadmap">Roadmap</button>
<button type="button" data-slipway-bearing-open="updates">What's new</button>Bearing listens with event delegation, so buttons rendered after the bootstrap
loads work too. Use feedback, roadmap, or updates as the requested surface.
Unknown or disabled surfaces do nothing safely.
Ordinary links remain ordinary navigation and are never intercepted. This makes it safe to offer the full Bearing page elsewhere, such as in the app footer:
<a href="https://your-app.example.com/bearing/feedback">Feedback</a>For a programmatic action, dispatch the equivalent event:
window.dispatchEvent(
new CustomEvent('slipway:bearing:open', {
detail: { surface: 'updates' }
})
)Set surface to feedback, roadmap, or updates. The requested surface is
temporary widget state: it does not change the host app URL or persist in
localStorage. Escape and an outside click close the panel even after the user
moves into the embedded surface, host-page scrolling stays locked while it is
open, and focus returns to the host control that invoked it.
Opening Feedback from the host app does not mark a product update as seen. The unread watermark changes only when the Updates surface is actually opened.
One host identity contract
Bearing resolves the same verified host-app identity as Bridge, but creates a
customer participant—not a Slipway user or Bridge access grant. Configure the
identity mapping once at slipway.identity; either feature may still override
it for an unusual app. Existing slipway.bridge.identity configuration remains
the backward-compatible fallback.
module.exports.slipway = {
identity: {
model: 'member',
sessionKey: 'memberId',
emailAttribute: 'emailAddress',
nameAttribute: 'name',
emailVerifiedAttribute: 'hasVerifiedEmail',
loginPath: '/sign-in'
}
}For tenant-aware or external authentication, let the app compute the login URL
instead of teaching Slipway whether the route is /login or /signin:
module.exports.slipway = {
identity: {
helper: 'slipway.identity',
loginHelper: 'slipway.loginUrl'
}
}The login helper receives req, returnUrl, and feature, and returns a safe
local URL containing whatever redirect state the app needs. A static
loginPath remains the conventional fallback.
Security model
- Bridge is disabled per app by default.
- The exchange credential is encrypted by Slipway, scoped to one app, kept server-side, and separate from the Lookout telemetry token.
- Enabling Bridge rotates the credential, so an old container cannot activate access before the required redeploy.
- Invitations expire after seven days, store only a SHA-256 token hash, and activate atomically against one host-app identity.
- Launch codes expire after two minutes and are consumed atomically once.
- The handoff regenerates the session before adding Bridge authorization.
- Disabling Bridge, revoking a grant, changing the app credential, or exceeding the eight-hour Bridge session lifetime invalidates access server-side.
- Bridge roles (
viewer,editor, andadministrator) are a ceiling. The target app's configured resource authorization can only narrow that access. - Declarative host authorization loads the host user by its server-established ID, never by a client-supplied email.
- OAuth provider tokens stay inside authentication and are not required by Bridge.
Lookout telemetry
When Lookout is configured by Slipway, the hook also sends a lightweight startup registration and a bounded heartbeat over the same authenticated telemetry endpoint. This lets Lookout distinguish a connected but quiet app from a missing or stale hook without keeping a socket open or retaining empty request spans. Slipway supplies the app and deployment identity automatically.
Slipway automatically injects the telemetry endpoint, scoped token, app ID, and deployment ID during deployment. Apps do not need to configure or expose those values themselves.
Optional settings live under lookout:
// config/slipway.js
module.exports.slipway = {
lookout: {
enabled: true,
batchSize: 50,
flushInterval: 10000,
captureQueries: true,
captureExceptions: true,
captureQuestEvents: true,
captureCache: true,
slowQueryThreshold: 100
}
}Telemetry failures never break the host application.
With sails-hook-quest 0.0.5 or newer, failed job output continues streaming
to the application log while its final bounded diagnostic is attached to the
Lookout exception. The Slipway hook removes terminal formatting and redacts
known secret values before telemetry leaves the application. Older Quest
payloads remain supported and use their runner stack when one is available.
Resident Quest workspace (0.0.12)
The new Quest workspace connects to the application's running Sails process to
inspect source jobs, review typed inputs, run a registered job, and pause or
resume its scheduler. Results and logs are shown separately. Scripts and
config/quest.js remain the source of truth for inputs and schedules.
This requires sails-hook-quest 0.0.6 or newer and
sails-hook-slipway 0.0.12 or newer. Quest 0.0.5 and Slipway hook 0.0.11 do not
include this contract. Older hooks retain bounded history in the dashboard,
with live state and resident controls marked unavailable. The
Quest upgrade guide
tracks the compatible pair and required verification; a version string alone never enables
unsupported controls.
Quest 0.0.6 currently needs the Sails ORM hook enabled. Database-free apps that exclude ORM can fail to finish startup; this is tracked in Quest issue #16.
After upgrading the two hooks and reviewing the application's registered jobs, explicitly enable the integration and deploy through the normal app workflow:
// config/slipway.js
module.exports.slipway = {
quest: { enabled: true }
}The setting defaults to false. Slipway verifies the app/deployment identity and the resident's capabilities before enabling controls. On Linux, the connection uses an app-scoped private Unix socket; it adds no public HTTP listener or new credential. Worker apps are supported. Multiple matching resident processes make controls unavailable because Slipway cannot choose a scheduler safely.
Pause prevents new runs in the current process and leaves active work running. It resets on app restart; change source configuration for a persistent schedule change. Closing or reconnecting the dashboard does not execute a job again. Use Run again to review and submit a deliberate new invocation. Completed means the child process exited successfully; a named Sails exit or business result can still describe an application-level failure.
Run history is bounded to seven days. Logs load on demand from retained tails; there is no live log replay, cancellation, durable delivery queue, automatic retry, or distributed overlap guarantee. Telemetry is best effort. A verified resident can reconcile a receipt it still holds after a connection loss, but a resident restart or receipt eviction can leave the outcome unconfirmed.
Release flags
Slipway injects the private flag endpoint and app identity during deployments and rollbacks. Evaluate a flag with an explicit safe default:
const enabled = await sails.helpers.flags.enabled.with({
key: 'new-checkout',
req: this.req,
defaultValue: false
})For a background job, pass a stable context containing a user, account,
tenant, or team identifier. Configuration is cached and refreshed in the
background; an unavailable control plane never fails or delays the host-app
request. flags.enabled is a regular Sails helper machine, so Sails validates
its declared inputs before evaluation. An app-defined helper at the same
identity is preserved. See the
release flag guide for
rollout behavior and Lookout comparisons.
Wake and read-only Bridge support views
Version 0.0.10 adds the full Wake collection/goals/revenue runtime and opt-in read-only support sessions. Both capabilities remain disabled until explicitly configured. See Wake setup, privacy, revenue replay and retention and Bridge support configuration and safety boundaries. Custom session mappings are inherited from slipway.identity; Wake also supports anonymous sessionless apps.
Quest reconnect and control (0.0.13)
This release integrates Quest 0.0.8 opt-in live logs and verified cancellation, plus bounded private receipt persistence. Node 22+ and Sails ^1.5.0 are required. Upgrade both hooks, retain existing telemetry configuration and explicitly configure the Linux controls and private persistent receipt directory. See CHANGELOG.md for the complete app upgrade instructions, bounds and uncertain-outcome behavior.
