Extension Points & Hooks
Sitepins Core is designed with a clean extension architecture. If you are building custom enterprise wrappers, adding proprietary authentication systems, or integrating with internal CRM/logging platforms, you can plug into Coreβs extension seams without modifying the core source code.
Backend Extension Seams
Core exports programmatic extension hooks located in api/src/lib/:
lib/lifecycleHooks.ts: User lifecycle events and audit handlers.lib/extensionGuards.ts: Request guards and response decorators, for custom quotas or policies.lib/mailer.ts: Custom transactional mail providers.lib/authIssuers.ts: External JWT issuer registration for machine-to-machine integrations.lib/authExtensions.ts: Better Auth plugin registration, for adding custom auth endpoints.startServer()inapi/src/server.ts: Allows an external script to register extensions before binding HTTP listeners.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Custom Server Wrapper / Startup Script β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β 1. setMailSender(customMailRelay) β β
β β 2. onUserRegistration(syncWithInternalCRM) β β
β β 3. registerJwtIssuer({ issuer, secret }) β β
β β 4. startServer() β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββββββββ
β Bootstraps
βΌ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Sitepins Core Backend β
β Express API β’ Hocuspocus β’ Socket.IO β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ1. Lifecycle & Event Hooks (lib/lifecycleHooks.ts)
You can attach custom handlers to key user actions:
import {
onUserRegistration,
onUserUpdate,
onUserDeletion,
onAuthEvent,
} from "./lib/lifecycleHooks";Event Hook Signatures
| Hook | Invocation Trigger | Payload Data | Error Handling |
|---|---|---|---|
onUserRegistration(fn) | Fired immediately after an account is created and verified (email OTP or OAuth). | { user: { id, email, full_name, provider } } | Isolated in a try/catch block. Hook failure will not prevent user registration. |
onUserUpdate(fn) | Fired when a user updates their profile (country or email). | { type: "country", email, country } or { type: "email", userId, oldEmail, newEmail } | Isolated in a try/catch block. |
onUserDeletion(fn) | Fired during permanent account deletion. | { userId, user, session } | Runs inside the Mongoose client session transaction. Pass session to your models to join the rollback transaction. |
onAuthEvent(fn) | Fired on user logins and password reset completions. | { type: "login" | "password_reset", userId, ip, date } | Isolated in a try/catch block. |
Quotas & Policies (lib/extensionGuards.ts)
Core never limits anything. To enforce your own project or organization quotas, register guards β throw an error (such as ApiError with status 400) to reject the request:
setRequestGuard(event, fn): Runs before"organization:create","organization:member:add", or"project:create".setProjectMutationGuard(fn): Runs before a projectβs visibility or status changes.setOrganizationDecorator(fn)/setProjectDecorator(fn): Post-process organization and project responses, for example to attach your own metadata.
2. Custom Transactional Mailers (lib/mailer.ts)
By default, Core uses Nodemailer SMTP (or terminal console fallback in dev). You can replace the mail transport completely:
import { setMailSender, type CustomMailSender } from "./lib/mailer";
const customMailer: CustomMailSender = async ({ to, kind, params }) => {
// kind: "welcome" | "otp" | "password_reset" | "delete_account" | "org_member_added" | etc.
console.log(`Sending ${kind} email to ${to}`);
// Deliver via your preferred API (AWS SES API, SendGrid, Postmark, etc.)
await sendViaProvider({
to,
template: kind,
variables: params,
});
};
// Register before starting server
setMailSender(customMailer);3. Registering External JWT Issuers (lib/authIssuers.ts)
If internal administrative dashboards or microservices need to invoke Core API routes using Bearer tokens:
import { registerJwtIssuer } from "./lib/authIssuers";
registerJwtIssuer({
issuer: "internal-backoffice",
secret: process.env.INTERNAL_BACKOFFICE_JWT_SECRET,
});When an incoming request carries an Authorization: Bearer <token> header, Coreβs authentication middleware iterates through all registered issuers and accepts the first token that validates.
4. Registering Better Auth Plugins (lib/authExtensions.ts)
To add authentication endpoints without forking core:
import { registerAuthPlugin } from "./lib/authExtensions";
registerAuthPlugin(myPlugin());Registered plugins are collected and passed to Better Auth when the auth instance is first constructed, so every call must happen before the auth handler serves its first request β register at startup, alongside your other extensions.
Core ships no partner or SaaS signup logic of its own; the hosted edition uses this seam to add POST /api/v1/auth/custom-signup.
5. Programmatic Server Startup
Core exports startServer() in api/src/server.ts and only executes automatically if invoked directly as the process entrypoint:
// Example custom bootstrap script (server-wrapper.ts)
import { setMailSender } from "./lib/mailer";
import { onUserRegistration } from "./lib/lifecycleHooks";
import { startServer } from "./server";
async function bootstrap() {
// 1. Attach custom hooks
onUserRegistration(async ({ user }) => {
console.log("New user registered:", user.email);
});
// 2. Launch the Express + Hocuspocus + Socket.IO server
await startServer();
console.log("Sitepins Core initialized with custom extensions.");
}
bootstrap();This clean architecture ensures your custom extensions survive upstream Git pulls and updates without merge conflicts.