Skip to content

About

Shared auth, profile and notification services reused by social, travel, workout and basa.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

platform-shared

Shared platform foundations for authentication, profiles, and notifications across social, travel, workout, and basa.

Documentation site: https://charles2ke.github.io/platform-shared/ (published from docs/).

  • Runtime: Node.js >= 22, ES modules, zero runtime dependencies.
  • Quality gates: CI test matrix (Node 22 and 24), syntax checks, npm audit, CodeQL default setup, Dependabot for npm and GitHub Actions.
  • Governance: SECURITY.md, CONTRIBUTING.md, CODE_OF_CONDUCT.md, issue/PR templates, and CODEOWNERS.

Architecture overview

This package is intentionally framework-light and dependency-free. Core business logic lives in reusable modules under src/, while app-specific HTTP, queue, or serverless integration should wrap these modules in each downstream repository.

src/
  auth/            JWT issuing/verification, refresh pattern, route guards, RBAC helpers
  profile/         Canonical profile model validation plus CRUD service and memory adapter
  notifications/   Unified send/schedule API, template rendering, channel adapters
  shared/          Environment config, structured errors, logging hooks
  adapters/        Framework/provider adapters: HTTP middleware, HTTP channel, worker loop
examples/          Integration stubs for social, travel, workout, and basa

Module responsibilities

Auth

  • Issues and verifies HMAC SHA-256 JWT access and refresh tokens.
  • Covers the full token lifecycle: issueTokenPair(), refreshAccessToken(), rotateTokenPair() for refresh-token rotation, and revocation through a TokenRevocationStore.
  • rotateTokenPair() detects refresh-token replay: presenting an already-rotated refresh token revokes every session for the subject (unless revokeSubjectOnReuse: false), invokes the optional onReuseDetected({ subject, payload }) hook, and throws AUTH_REFRESH_TOKEN_REUSED.
  • decodeToken() inspects a token without verifying it (logging/debugging only), and describeToken() summarizes a verified payload (issuedAt, expiresAt, expiresInSeconds, expired) for session/introspection endpoints.
  • InMemoryTokenRevocationStore supports single-token revocation (revokeToken()), "log out everywhere" (revokeSubject()), and prune() for expired entries. Implementations must be synchronous so guards stay synchronous.
  • Provides createAuthGuard() and protect() helpers for framework-specific route wrappers. Guards accept revocationStore and roleRegistry so revocation and RBAC are enforced on every request.
  • issueTokenPair() stamps a shared session id (sid) on the access and refresh token and returns it as sessionId, so one login can be revoked as a unit. revokeSession(payload, { revocationStore }) ends that session (logout) and falls back to single-token revocation for tokens issued without a sid. Each rotateTokenPair() call starts a new session unless sessionId is passed; replaying a rotated refresh token also revokes the replayed session.
  • Includes RBAC scaffolding with role and permission checks, a createRoleRegistry() role-to-permission map with inheritance, resolvePrincipal() for expanding token roles into permissions, and authorize() for enforcement outside route guards.
  • Role inheritance also applies to role checks: registry.rolesFor() expands inherited roles and resolvePrincipal() stores them on principal.effectiveRoles (token roles stay untouched), so a coach that inherits athlete satisfies { roles: ['athlete'] }.
  • createAccessPolicy({ 'profile.update': { permissions: ['profile:write'] } }) maps action names to requirements so RBAC is enforced inside services, jobs, and queue consumers, not only on HTTP routes. ProfileService and NotificationService accept the resulting policy (or a plain requirement map) and a roleRegistry; callers then pass { principal } per call. Actions without requirements stay open; a missing principal fails with AUTH_PRINCIPAL_REQUIRED unless requirePrincipal: false.
  • Exposes an AccountStore interface plus an InMemoryAccountStore default; MongoAccountStore (in storage) is a persistent adapter with case-insensitive findByEmail() backed by a unique index (duplicate emails fail with AUTH_ACCOUNT_EMAIL_CONFLICT, 409).
  • createLoginThrottle({ maxAttempts, windowMs, lockoutMs, maxEntries }) slows password guessing: recordFailure(key) counts failures per caller-chosen key (account, IP, or both), assertAllowed(key) throws AUTH_LOGIN_LOCKED (429, details.retryAfterSeconds) while a key is locked out, and recordSuccess(key) clears it. It is synchronous, in-process, and memory-bounded; scaled-out deployments that need a shared view should provide a shared implementation of the same methods.

Profile

  • Uses a canonical profile shape: id, displayName, contact, preferences, timezone, locale, avatarUrl, status, and metadata.
  • Normalizes display names, email addresses, locale/timezone defaults, and status.
  • Provides validation helpers and ProfileService over the replaceable ProfileStore interface.
  • Ships InMemoryProfileStore for tests, prototypes, and local development.
  • softDelete(id) marks a profile deleted with a deletedAt timestamp (enforces profile.delete); restore(id, { status }) brings it back (default active; enforces profile.restore, or profile.delete when no profile.restore requirement is configured). delete(id) still removes permanently. update() applies the same rules when a status change moves a profile into or out of deleted (extra policy check and deletedAt bookkeeping), so profile.update alone cannot soft-delete or restore.
  • search({ query, status, includeDeleted, limit, cursor }) matches query case-insensitively against display name and email, hides soft-deleted profiles by default, and returns { items, nextCursor } ordered by id (enforces profile.search, or profile.list when not configured). Stores may implement search() natively (InMemoryProfileStore, MongoProfileStore, and CachedProfileStore do); otherwise the service filters store.list(), which is only complete if list() returns every profile.

Notifications

  • Provides one API for sending or scheduling notifications.
  • Supports email, SMS, and push channel abstraction.
  • strategy: 'fallback' tries channels in order and stops at the first channel that does not fail (for example push, then SMS, then email); the result is sent if any channel delivered. The default strategy: 'all' delivers on every channel.
  • Quiet hours: when a notification carries quietHours: { start: '22:00', end: '07:00', timezone }, send() queues it for the end of the window (result status: 'pending', deferred: true) and schedule() shifts times that fall inside the window. bypassQuietHours: true sends urgent messages immediately. quietHoursFromProfile(profile) reads profile.preferences.quietHours and defaults the timezone to profile.timezone. Quiet hours are evaluated at send()/schedule() time; retries queued by dispatchScheduled() are not re-checked.
  • Renders {{variable}} template placeholders from provided variables.
  • Returns delivery status objects with sent, failed, partial, or pending states.
  • Includes an in-memory scheduling workflow via schedule() and dispatchScheduled() for queue handoff patterns.
  • Ships a NotificationScheduler interface plus InMemoryNotificationScheduler (enqueue/dequeueDue/requeue/countPending/list) as a reference queue adapter.
  • Retries use configurable exponential backoff: retryDelayMs * retryBackoffFactor ** (attempts - 1), capped by maxRetryDelayMs. retryDelayFor(attempts) exposes the computed delay.
  • Exhausted retries (or schedulers without requeue()) call the optional onDeadLetter({ notification, attempts, reason }) hook; dispatchScheduled() reports retried and deadLettered counts and per-result attempts, retryScheduledFor, and deadLettered.
  • dispatchScheduled() returns pending plus pendingKnown; when an injected scheduler does not expose countPending(), pending is 0 and pendingKnown is false.
  • In-memory retries honor maxScheduleAttempts and retryDelayMs; injected schedulers can support retries by implementing requeue().
  • maxScheduleAttempts bounds total delivery attempts (initial attempt included). Scheduler adapters should return wrapped due entries as { notification, scheduledFor } (or { notification, when }).
  • listScheduled() returns pending entries and cancelScheduled(notificationId) drops queued deliveries (trip cancelled, workout completed early); injected schedulers must implement list() and cancel() for these.
  • Partial failures are retried per channel: only the channels that failed are re-queued (result.retryChannels), so delivered channels are never sent twice. Set retryPartialFailures: false to keep the previous behavior.
  • A deadLetterStore (see DeadLetterStore / InMemoryDeadLetterQueue) persists { notification, attempts, reason, channels, failedAt } records for exhausted retries so failures can be inspected and replayed with replayDeadLetters().
  • Enforces the shared access policy on send(), schedule(), listScheduled(), cancelScheduled(), and dispatchScheduled() when policy is supplied.
  • Defines a ChannelAdapter interface and ships MockChannelAdapter for default/local provider behavior.

Adapters

  • createExpressAuthMiddleware(guard, { requirements }) mounts a guard in Express/Connect apps and answers with the shared error envelope; withFetchAuth(handler, guard, requirements) does the same for fetch-style route handlers (Next.js, Hono, workers).
  • toHttpErrorResponse(error) maps any error to { status, body } for consistent API error responses.
  • HttpChannelAdapter is a concrete channel adapter that POSTs rendered notifications to a provider endpoint, with headers, transform, and timeoutMs options.
  • createNotificationWorker(service, { intervalMs }) drains scheduled notifications from a cron trigger (runOnce()) or a long-running worker (start() / stop()), and replayDeadLetters({ service, store }) re-queues dead-lettered notifications after an outage.

Runtime

  • createBackgroundWorker({ handler, intervalMs }) is a generic background job runner: schedule it with start() / stop() or trigger it on demand with runOnce() from an HTTP route, CLI command, or external cron. Overlapping runs are skipped, timeoutMs bounds a run, failures are normalized to PlatformError (and forwarded to onError instead of thrown when supplied), and getStats() exposes run counters for metrics and health endpoints.

Cache

  • createRedisCache({ client, ttlSeconds, ttlByPrefix, metrics }) is a fail-open two-tier cache (bounded in-process LRU in front of an injected Redis client) with single-flight getOrLoad(). When a metrics registry is supplied it records cache_hits_total, cache_misses_total, and cache_errors_total.
  • ttlByPrefix: { 'profile:': 600, 'profile:hot:': 30 } sets default TTLs per key prefix; the longest matching prefix wins, an explicit set(key, value, { ttl }) still overrides, and ttlFor(key) reports the effective TTL.
  • CachedProfileStore wraps any ProfileStore with read-through caching and passes search() through uncached.

Shared

  • loadConfig() reads environment configuration.
  • PlatformError gives structured error objects with code, status, message, and details.
  • Logger hooks accept any object with debug, info, warn, and error methods.

Environment variables

Copy .env.example and set values in each app environment.

Variable Required Purpose
PLATFORM_JWT_SECRET Production yes JWT HMAC secret; use at least 32 random characters.
PLATFORM_JWT_ISSUER No JWT issuer, defaults to platform-shared.
PLATFORM_JWT_AUDIENCE No App or API audience to verify.
PLATFORM_JWT_ACCESS_TTL_SECONDS No Access token TTL, defaults to 900 seconds.
PLATFORM_JWT_REFRESH_TTL_SECONDS No Refresh token TTL, defaults to 30 days.
PLATFORM_DEFAULT_LOCALE No Profile default locale, defaults to en-US.
PLATFORM_DEFAULT_TIMEZONE No Profile default timezone, defaults to UTC.
PLATFORM_DEFAULT_FROM_EMAIL No Default email sender for provider adapters.
PLATFORM_DEFAULT_SMS_SENDER No Default SMS sender for provider adapters.
PLATFORM_DEFAULT_PUSH_SENDER No Default push sender for provider adapters.

Usage snippets

Issue and verify auth tokens

import { issueTokenPair, createAuthGuard } from '@charles2ke/platform-shared/auth';

const tokens = issueTokenPair({
  subject: 'user-123',
  roles: ['member'],
  permissions: ['profile:read'],
  secret: process.env.PLATFORM_JWT_SECRET,
  issuer: 'platform-shared',
  audience: 'social'
});

const guard = createAuthGuard({
  secret: process.env.PLATFORM_JWT_SECRET,
  issuer: 'platform-shared',
  audience: 'social'
});

const principal = guard(request, { permissions: ['profile:read'] });

Rotate, revoke, and map roles to permissions

import {
  createAuthGuard,
  createRoleRegistry,
  describeToken,
  InMemoryTokenRevocationStore,
  rotateTokenPair
} from '@charles2ke/platform-shared/auth';

const revocationStore = new InMemoryTokenRevocationStore();
const roleRegistry = createRoleRegistry({
  member: ['profile:read'],
  moderator: { permissions: ['post:delete'], inherits: ['member'] }
});

const guard = createAuthGuard({ secret, issuer: 'platform-shared', audience: 'social', revocationStore, roleRegistry });

// Refresh endpoint: rotate the pair, revoke the presented refresh token, and
// treat a replayed refresh token as a compromise (all sessions are revoked).
const rotated = rotateTokenPair(refreshToken, {
  secret,
  issuer: 'platform-shared',
  audience: 'social',
  revocationStore,
  onReuseDetected: ({ subject }) => securityLog.warn('refresh token replay', { subject })
});

// Session/introspection endpoint.
const session = describeToken(principal.claims); // { expiresAt, expiresInSeconds, expired, ... }

// Inherited roles are available on principal.effectiveRoles.
guard(request, { roles: ['member'] });

// Logout endpoints.
revokeSession(principal.claims, { revocationStore }); // ends this login (access + refresh)
revocationStore.revokeToken(principal.claims);        // single token
revocationStore.revokeSubject('user-123');            // everywhere

Enforce RBAC inside services, jobs, and consumers

import { createAccessPolicy } from '@charles2ke/platform-shared/auth';
import { ProfileService } from '@charles2ke/platform-shared/profile';

const policy = createAccessPolicy({
  'profile.get': { permissions: ['profile:read'] },
  'profile.update': { permissions: ['profile:write'] },
  'profile.delete': { roles: ['admin'] }
}, { roleRegistry });

const profiles = new ProfileService({ store, policy });

await profiles.update(id, { timezone: 'Africa/Nairobi' }, { principal });

Mount guards and providers in a consumer repo

import { createExpressAuthMiddleware, createNotificationWorker, HttpChannelAdapter, replayDeadLetters, withFetchAuth } from '@charles2ke/platform-shared/adapters';

app.get('/feed', createExpressAuthMiddleware(guard, { requirements: { roles: ['member'] } }), handler);

export const GET = withFetchAuth(routeHandler, guard, { permissions: ['profile:read'] });

const notifications = new NotificationService({
  adapters: { email: new HttpChannelAdapter({ channel: 'email', endpoint: process.env.EMAIL_WEBHOOK_URL, headers: { authorization: providerKey } }) },
  scheduler,
  deadLetterStore
});

const worker = createNotificationWorker(notifications, { intervalMs: 60_000 });
worker.start();

await replayDeadLetters({ service: notifications, store: deadLetterStore });

Manage profiles

import { ProfileService } from '@charles2ke/platform-shared/profile';

const profiles = new ProfileService({ defaults: { locale: 'en-US', timezone: 'UTC' } });

const profile = await profiles.create({
  displayName: 'Charles',
  contact: { email: '[email protected]' },
  preferences: { units: 'metric' }
});

await profiles.softDelete(profile.id);
await profiles.restore(profile.id);

const page = await profiles.search({ query: 'charles', limit: 20 });
const next = await profiles.search({ query: 'charles', limit: 20, cursor: page.nextCursor });

Plug in persistent stores and providers

import { ProfileStore } from '@charles2ke/platform-shared/profile';
import { ChannelAdapter } from '@charles2ke/platform-shared/notifications';

class SqlProfileStore extends ProfileStore {
  async create(profile) { /* app-owned persistence */ }
  async get(id) { /* ... */ }
  async update(id, profile) { /* ... */ }
  async delete(id) { /* ... */ }
  async list() { /* ... */ }
}

class EmailProviderAdapter extends ChannelAdapter {
  async send(message) { /* call provider, return a delivery status object */ }
}

Unimplemented interface methods throw structured PlatformErrors instead of failing silently.

Fallback channels and quiet hours

import { NotificationService, quietHoursFromProfile } from '@charles2ke/platform-shared/notifications';

await notifications.send({
  id: 'trip-reminder-42',
  channels: ['push', 'sms', 'email'],
  strategy: 'fallback',
  quietHours: quietHoursFromProfile(profile), // e.g. 22:00-07:00 in the profile's timezone
  body: 'Your flight boards in 3 hours'
});

Throttle failed logins

import { createLoginThrottle } from '@charles2ke/platform-shared/auth';

const throttle = createLoginThrottle({ maxAttempts: 5, windowMs: 15 * 60_000, lockoutMs: 15 * 60_000 });

function login(email, password, ip) {
  const keys = [`account:${email.toLowerCase()}`, `ip:${ip}`];
  keys.forEach((key) => throttle.assertAllowed(key)); // throws AUTH_LOGIN_LOCKED (429)
  if (!checkPassword(email, password)) {
    keys.forEach((key) => throttle.recordFailure(key));
    throw new Error('Invalid credentials');
  }
  keys.forEach((key) => throttle.recordSuccess(key));
}

Send notifications

import { CHANNELS, MockChannelAdapter, NotificationService } from '@charles2ke/platform-shared/notifications';

const notifications = new NotificationService({
  adapters: {
    [CHANNELS.EMAIL]: new MockChannelAdapter({ channel: CHANNELS.EMAIL }),
    [CHANNELS.PUSH]: new MockChannelAdapter({ channel: CHANNELS.PUSH })
  }
});

await notifications.send({
  channels: [CHANNELS.EMAIL, CHANNELS.PUSH],
  to: { email: '[email protected]', userId: 'user-123' },
  subject: 'Welcome {{name}}',
  body: 'Hi {{name}}, your account is ready.',
  variables: { name: 'Charles' }
});

Schedule notifications with retries

import { InMemoryNotificationScheduler, NotificationService } from '@charles2ke/platform-shared/notifications';

const scheduler = new InMemoryNotificationScheduler(); // swap for a queue-backed adapter
const notifications = new NotificationService({
  adapters,
  scheduler,
  maxScheduleAttempts: 4,
  retryDelayMs: 30_000,
  retryBackoffFactor: 2,
  maxRetryDelayMs: 15 * 60_000,
  deadLetterStore, // durable record of exhausted retries, replayable later
  onDeadLetter: ({ notification, attempts, reason }) => auditLog.write({ notification, attempts, reason })
});

await notifications.schedule(reminder, new Date(Date.now() + 60_000));

// Cron/worker loop
const { processed, retried, deadLettered, pending } = await notifications.dispatchScheduled();

// Cancel queued deliveries when the underlying event changes.
await notifications.listScheduled();
await notifications.cancelScheduled(reminder.id);

Run a background job on a schedule or on demand

import { createBackgroundWorker } from '@charles2ke/platform-shared/runtime';

const worker = createBackgroundWorker({
  name: 'profile-reindex',
  handler: async ({ trigger }) => reindexProfiles({ trigger }),
  intervalMs: 5 * 60_000,
  timeoutMs: 60_000,
  runOnStart: true,
  logger,
  onError: (error) => metrics.increment('profile_reindex_failed', { code: error.code })
});

worker.start(); // long-running process

// On demand: admin endpoint, CLI command, or external cron trigger.
const run = await worker.runOnce({ trigger: 'admin', requestedBy: principal.id });
if (run.status === 'skipped') {
  // a run was already in flight
}

worker.getStats(); // { runs, failures, skipped, lastDurationMs, ... }
worker.stop();

Downstream integration approach

  1. Install or vendor this package in the consuming app.
  2. Load app-specific env vars with loadConfig() or the app's existing config layer.
  3. Wrap createAuthGuard() in the app's router middleware layer.
  4. Replace in-memory stores/adapters with app-owned persistence and provider adapters by extending AccountStore, ProfileStore, ChannelAdapter, NotificationScheduler, or TokenRevocationStore (or supplying objects with the same methods).
  5. Keep domain-specific behavior in the app; keep shared identity/profile/notification contracts here.

See examples/social.js, examples/travel.js, examples/workout.js, and examples/basa.js for starting points. They are executable stubs covered by tests/examples.test.js:

Example Shows
social.js Login, guarded routes (including Express middleware), refresh-token rotation with replay detection, session introspection, session/single-token/global logout, role registry.
workout.js Role-derived and inherited-role permissions, authorize() in service code, scheduled push nudges with exponential backoff, cancellation, and dead lettering.
travel.js Immediate and scheduled multi-channel reminders drained by createNotificationWorker(), plus pending listing, cancellation, dead-letter capture, and replay.
basa.js RBAC-guarded order operations, policy-enforced profile CRUD (createAccessPolicy()), and scheduled email/SMS order updates with pending listing and cancellation.

Migration/adoption checklist

social

  • Use shared JWT issuing/verification for login and protected social routes.
  • Map existing user profile fields to the canonical profile model.
  • Replace local notification calls with NotificationService channel adapters.
  • Add social-specific persistent account/profile store adapters.

travel

  • Verify API/mobile tokens with createAuthGuard() and travel audience settings.
  • Store traveler locale/timezone/preferences through ProfileService.
  • Route trip reminders through email and push adapters.
  • Add provider-specific retry/queue integration around notification delivery.

workout

  • Guard workout read/write routes with shared permission names.
  • Use profile preferences for units, timezone, and notification settings.
  • Send workout nudges through push notifications.
  • Add persistent adapters for workout user profiles.

basa

  • Align customer account auth with shared token claims and RBAC scaffolding.
  • Normalize customer contact metadata with the canonical profile model.
  • Route order updates through email/SMS adapters.
  • Add commerce-specific permissions and persistent stores.

Testing and local run instructions

This repository uses Node's built-in test runner and has no external runtime dependencies.

npm test           # unit and example tests
npm run build      # syntax checks across src, tests, examples, scripts
npm run check      # build + test (run this before opening a pull request)
npm run test:coverage  # tests with Node's experimental coverage report
npm run audit          # production dependency audit (high severity and above)

npm run build performs syntax checks across source, tests, examples, and scripts.

TypeScript declarations

Type declarations are generated from the JSDoc in src/ with the typescript dev dependency and are not committed:

npm run types        # emit .d.ts files into types/ (also runs on npm install/prepare and before publish)
npm run check:types  # type-check the TypeScript consumer tests against the JSDoc-derived types

Every exports subpath has a matching types entry, so import { verifyToken } from '@charles2ke/platform-shared/auth' is typed in TypeScript and editor tooling.

Enterprise readiness

Dependencies and supported runtimes

  • The package has no runtime dependencies and no build-time dependencies, so there is no transitive supply-chain surface to patch. A committed package-lock.json keeps installs reproducible, and npm audit --omit=dev --audit-level=high runs in CI and reports no advisories.
  • engines.node is >=22; CI verifies Node 22 and Node 24.
  • Dependabot watches npm and GitHub Actions weekly, and all workflows pin the latest stable major versions of the actions they use.

Automated controls

Control Where Trigger
Tests + syntax checks on Node 22/24 .github/workflows/ci.yml push, pull request, manual
Production dependency audit .github/workflows/ci.yml push, pull request, manual
CodeQL analysis GitHub code scanning default setup push, pull request, weekly
Dependency version updates .github/dependabot.yml weekly
Documentation site deploy .github/workflows/pages.yml push to main touching docs/

Workflows declare least-privilege permissions and check out without persisting credentials.

Hardening built into the modules

  • HS256 signatures are compared in constant time, and issuer, audience, and expiry claims are verified explicitly.
  • Refresh-token rotation detects replay and revokes the affected sessions.
  • Revocation stores are synchronous, so a guard cannot be bypassed by an unawaited promise.
  • Access policies are action-keyed, so the same requirements apply to HTTP routes, jobs, and queue consumers.
  • Errors are structured PlatformErrors, and toHttpErrorResponse() maps them to a consistent { status, body } envelope; it does not sanitize error.message or PlatformError.details, so callers must avoid putting sensitive data in either.

Operator checklist

  • PLATFORM_JWT_SECRET is at least 32 random characters, stored in a secret manager, and rotated on a schedule.
  • Token revocation is backed by a shared store or cache when running more than one instance.
  • Access-token TTLs stay short and refresh rotation is enabled.
  • TLS terminates in front of every guarded service.
  • Dead-letter records are monitored and replayed after provider outages.

See SECURITY.md for the vulnerability reporting process and the full security model.

Documentation site

docs/index.html is a dependency-free single-page site covering every module, feature, and hardening control in this repository. It is deployed to GitHub Pages by .github/workflows/pages.yml whenever docs/ changes on main. Enable it once under Settings → Pages → Build and deployment → GitHub Actions. Update the site alongside README.md whenever public behavior changes.

Architecture decisions and next steps

  • JWT support is implemented with built-in Node crypto and HS256 to avoid introducing dependencies before provider decisions are made.
  • Guards are request-shape based instead of Express/Fastify/Next specific, so each app can adapt them to its framework.
  • Store and channel adapters are constructor-injected to make persistence and provider migration explicit.
  • Notification scheduling supports in-memory queuing for local workflow depth; downstream apps should still connect production queues/schedulers through adapters.
  • Token revocation is an injectable synchronous store so guards remain synchronous; production apps back it with a cache warmed from their session store.
  • Access and refresh tokens carry a shared session id so logout and refresh-token replay can close a whole login without revoking every session for a subject.
  • Authorization is expressed as action-keyed policies rather than hard-coded checks, so the same requirements apply to HTTP routes, background jobs, and queue consumers.
  • Framework and provider glue lives in src/adapters/ so the core modules stay framework-light while consumer repos get working starting points.

Recommended next steps:

  1. Publish the package or configure each app to consume it from GitHub.
  2. Add persistent adapters for each app's user/account/profile data store.
  3. Add real email, SMS, and push adapters behind the existing notification channel interface (HttpChannelAdapter covers HTTP providers).
  4. Standardize role and permission names across social, travel, workout, and basa.

About

Shared auth, profile and notification services reused by social, travel, workout and basa.

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages