@navio-maritime/platform
v0.12.0
Published
Shared platform primitives for every Navio Maritime product: JWT verification, vessel context, invite helpers, Pattern 3+4 cache invalidation, PIN signoff trust window, identity-plane grace-period verifier. Consumed by NavioWorks, NavioFit, NavioBuild, MA
Readme
@navio/platform
Shared platform primitives for every Navio Maritime product: Supabase JWT verification, vessel context resolution, and vessel invite issuance / acceptance. One consistent integration layer across NavioWorks, NavioFit, NavioBuild, MANTIS, and BEACON.
What it is
Three primitive surfaces, landed one per PR under the design spec
memory/project_navio_platform_package_design.md:
- Auth — verify a Supabase JWT, classify failures with a typed error code, cache verified tokens to avoid repeat round-trips.
- Vessel — resolve a full
VesselContext(user + vessel + role + department) from an authsubclaim, or pick the user's active vessel when no explicitvesselIdis passed. - Invites — issue a Supabase magic-link invite tied to a vessel grant, and accept the resulting token after the user completes password setup.
Every primitive accepts an optional supabase client for testing and
lazy-constructs a shared admin client from env vars when called
live. No framework coupling — use it from Express, Hono, tRPC, an
edge function, a CLI.
What it's NOT
- Not a schema owner. The package reads/writes
users,vessel_users, andvesselsrows; the canonical schema lives in each product's database migrations. - Not business logic. No crew, equipment, tasks, IHM, SEEMP, or any other product-specific domain. Those stay in their respective routers.
- Not UI. Components + design tokens live in
@navio-maritime/ui(packages/ui-navio/, M4 — published 0.1.0). - Not a Supabase client wrapper. Primitives delegate to
@supabase/supabase-js; callers who already hold aSupabaseClientcan inject it to avoid double-caching.
Public API (v0.4.0)
| Export | Kind | Description |
| ----------------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| verifySupabaseToken(token) | fn | HS256-first JWT verification with Supabase API fallback + 60s cache. Throws PlatformAuthError with typed code. |
| getVesselContext(userId, vesselId?, options?) | fn | Resolve the full VesselContext for a Supabase auth sub. Throws PlatformVesselError on no access / missing vessel / no assignments. |
| resolveActiveVessel(userId, options?) | fn | Non-throwing variant — returns the primary (or first available) vessel_id for a user, or null. |
| createVesselInvite(params, options?) | fn | Issue a vessel invite. New email → creates Supabase auth user + sends magic-link email + creates vessel_users row. Existing user → vessel_users row only. Throws PlatformInviteError on duplicates / missing vessel. |
| acceptVesselInvite(token, options?) | fn | Post-login lookup primitive. Validates token state (invalid / expired / already-accepted) and returns the VesselContext the invitee should land in. |
| PlatformAuthError | class | extends Error with typed code: PlatformAuthErrorCode. |
| PlatformVesselError | class | Same shape; code: PlatformVesselErrorCode. |
| PlatformInviteError | class | Same shape; code: PlatformInviteErrorCode. |
Type exports: PlatformUser, PlatformUserRole, PlatformVessel,
VesselContext, VesselRole, VesselDepartment, VesselInvite,
CreateVesselInviteParams, CreateVesselInviteOptions,
AcceptVesselInviteOptions, plus the *ErrorCode string unions.
Installation
From GitHub Packages (external repos — MANTIS, BEACON, etc.)
Add a registry entry for the
@navioscope in the consumer repo's.npmrc:@navio:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}Provide a
GITHUB_TOKENwithread:packagesscope in the consumer environment. For local dev, use a PAT (GITHUB_TOKEN=ghp_...in a shell.env). For CI, the built-inGITHUB_TOKENfrom Actions is sufficient if the consumer repo is in the same org.Install:
pnpm add @navio/platform
From this monorepo (NavioWorks apps)
Already wired via pnpm workspace resolution — add to package.json:
"dependencies": {
"@navio/platform": "workspace:*"
}Quick start
Express middleware that verifies a Supabase session and loads the vessel context onto the request:
import { Request, Response, NextFunction } from 'express';
import {
verifySupabaseToken,
getVesselContext,
PlatformAuthError,
PlatformVesselError,
type VesselContext,
} from '@navio/platform';
declare module 'express-serve-static-core' {
interface Request {
ctx?: VesselContext;
}
}
export async function requireVessel(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
const token = req.headers.authorization?.replace(/^Bearer\s+/i, '');
if (!token) return void res.status(401).json({ code: 'no_token' });
try {
const user = await verifySupabaseToken(token);
const vesselIdHeader = req.header('x-vessel-id') ?? undefined;
req.ctx = await getVesselContext(user.userId, vesselIdHeader);
next();
} catch (err) {
if (err instanceof PlatformAuthError) {
res.status(401).json({ code: err.code, message: err.message });
return;
}
if (err instanceof PlatformVesselError) {
res.status(403).json({ code: err.code, message: err.message });
return;
}
next(err);
}
}The same pattern works for Hono, tRPC createContext, or a Supabase
Edge Function — swap the framework glue; keep the primitive calls.
Environment
Read from process.env lazily at call time (testable via
vi.stubEnv):
| Var | Used by | Required? |
| --------------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| SUPABASE_URL | all primitives | Yes (unless a client is injected via options.supabase) |
| SUPABASE_SERVICE_ROLE_KEY | all primitives | Yes (same) |
| JWT_SECRET | verifySupabaseToken | Optional — enables the HS256 fast path. Without it, every verification falls back to the Supabase API. |
| APP_URL | createVesselInvite | Optional — default http://localhost:3001. Used as the base for the Supabase invite redirectTo when the caller doesn't pass params.redirectTo. |
Missing Supabase env vars do not throw at import time — they throw
(or return null, for resolveActiveVessel) when a primitive
actually tries to call a client. This keeps the package importable
from tests and CI without a live Supabase instance.
Versioning
Manual semver per PR. Every public-API change bumps the version in
package.json and adds a CHANGELOG.md entry in the same commit
that ships the change. Breaking changes (rename, removal, signature
change) are major bumps; additive surface is minor; internal-only
fixes are patch.
Changesets adoption is planned for Great Alignment Week 8+ (per design memo decision #3) — not yet.
Contributing
See the repo's CONTRIBUTING.md. Public API is governed by the
design spec at
memory/project_navio_platform_package_design.md —
any new export must be justified there before it lands.
Publishing
First publish is a manual GitHub Actions dispatch by a maintainer.
See .github/workflows/publish-platform.yml:
Actions → Publish @navio/platform → Run workflow → dry_run=falseThe workflow rebuilds, re-tests, verifies the version doesn't already
exist in the registry, then publishes to
https://npm.pkg.github.com. Auto-trigger on push/tag is deliberately
not wired — a non-publish-ready commit landing on main should not
publish automatically.
License
UNLICENSED. This is a private package distributed inside Navio Maritime OÜ's GitHub organisation. External redistribution is not permitted.
