Adapter Hooks
Plugins contribute Adapter hooks through OberonPluginAdapterHooks:
type OberonPluginAdapterHook<Key extends keyof OberonPluginAdapter> = (context: {
getAdapter: () => Omit<OberonAdapter, Key>
next: OberonPluginAdapter[Key]
}) => OberonPluginAdapter[Key]Each hook is initialized once with the final Adapter getter and the preceding next implementation,
then returns an Adapter method with its original signature. Methods with input receive one object:
updatePageData receives a Page, while putKV receives { namespace, key, value }. Methods
without input remain zero-argument functions.
Call getAdapter() from the returned method when it executes, not while the hook factory
initializes. The getter throws until composition is complete, then returns the same final augmented
Adapter used by Handlers and actions. Its current method is omitted from the getter’s static return
type to prevent accidental self-recursion. Use next to continue the current method’s middleware
chain.
Later plugins are outermost. For [storage, tailwind, seo, framework], updatePageData enters
framework, seo, tailwind, then storage, and unwinds in reverse. A direct provider ignores
next and returns its implementation; middleware returns a function that calls next before or
after its own behavior.
Bootstrap hooks
Lifecycle work used during setup and build is exposed as a top-level bootstrap hook, not as
adapter middleware.
type OberonPlugin = ({ phase }: { phase: OberonPluginPhase }) => {
bootstrap?: (context: { adapter: OberonAdapter }) => Promise<void>
}Use bootstrap for migrations or initialization required before build output. Enabled hooks are
awaited once in configured plugin order. If one throws, later hooks and core Page/Site
initialization do not run.
OberonDatabaseAdapter
Database plugins own the persistence surface.
Content and media methods
addPagedeletePagegetAllPagesgetPageDataupdatePageDataaddImagedeleteImagegetAllImages
Site and key-value methods
getSiteupdateSitegetKVputKVdeleteKV
User methods
addUserdeleteUsergetAllUserschangeRole
OberonCanAdapter
Permission and session helpers:
getCurrentUserhasPermissionsignInsignOut
hasPermission receives user, action, and permission, and returns a boolean synchronously.
The final Adapter also provides can, will, and whoWill. Actions use these helpers to enforce
permissions without making persistence methods authorize themselves.
OberonAuthAdapter
Auth storage and extensions:
getAuthDatabasegetAuthPlugins
OberonRoutingAdapter
Framework integrations provide request and navigation behavior:
redirectnotFoundgetRequestHeaders
OberonSendAdapter
Send plugins provide verification delivery:
sendVerificationRequest
type OberonSendAdapter = {
sendVerificationRequest: (props: { email: string; token: string; url: string }) => Promise<void>
}Minimal custom implementations
Database-oriented custom plugin:
import { USE_DEVELOPMENT_DATABASE_PLUGIN, notImplemented, type OberonPlugin } from "@oberoncms/core"
import { migrate } from "./database"
export const databasePlugin: OberonPlugin = () => ({
name: "Custom Database Plugin",
disabled: USE_DEVELOPMENT_DATABASE_PLUGIN,
bootstrap: async ({ adapter }) => {
await migrate(adapter)
},
adapter: {
addPage: () => notImplemented("addPage"),
addImage: () => notImplemented("addImage"),
addUser: () => notImplemented("addUser"),
deletePage: () => notImplemented("deletePage"),
deleteImage: () => notImplemented("deleteImage"),
deleteKV: () => notImplemented("deleteKV"),
deleteUser: () => notImplemented("deleteUser"),
changeRole: () => notImplemented("changeRole"),
getAllImages: () => notImplemented("getAllImages"),
getAllPages: () => notImplemented("getAllPages"),
getAllUsers: () => notImplemented("getAllUsers"),
getKV: () => notImplemented("getKV"),
getPageData: () => notImplemented("getPageData"),
getSite: () => notImplemented("getSite"),
putKV: () => notImplemented("putKV"),
updatePageData: () => notImplemented("updatePageData"),
updateSite: () => notImplemented("updateSite"),
},
})Send-oriented custom plugin:
import "server-cli-only"
import { USE_DEVELOPMENT_SEND_PLUGIN, type OberonPlugin } from "@oberoncms/core"
const EMAIL_FROM = process.env.EMAIL_FROM
const SEND_SECRET = process.env.SEND_SECRET
async function sendEmail(
message: {
from: string
subject: string
text: string
to: string
},
secret: string,
) {
void message
void secret
throw new Error("sendEmail is not implemented")
}
export const sendPlugin: OberonPlugin = () => ({
name: "Custom Send",
disabled: USE_DEVELOPMENT_SEND_PLUGIN,
adapter: {
sendVerificationRequest:
() =>
async ({ email, token, url }) => {
if (!SEND_SECRET) throw new Error("No SEND_SECRET configured")
if (!EMAIL_FROM) throw new Error("No EMAIL_FROM configured")
await sendEmail(
{
from: EMAIL_FROM,
subject: "One time login to Oberon CMS",
text: `Sign in with code\n\n${token}\n\n${url}\n\n`,
to: email,
},
SEND_SECRET,
)
},
},
})