@hookbase/portal
The Hookbase Portal provides embeddable React components for customer-facing webhook management.
Installation
bash
npm install @hookbase/portalQuick Start
tsx
import { HookbasePortal, EndpointList, EndpointForm } from '@hookbase/portal';
import '@hookbase/portal/styles.css';
function WebhookSettings({ portalToken }) {
return (
<HookbasePortal token={portalToken}>
<EndpointForm />
<EndpointList />
</HookbasePortal>
);
}Getting a Portal Token
Generate tokens server-side using the SDK:
typescript
// Backend API endpoint
app.get('/api/webhook-portal-token', async (req, res) => {
const customer = await getCustomerFromSession(req);
const token = await hookbase.portalTokens.create(customer.hookbaseAppId, {
expiresIn: 3600, // 1 hour
});
res.json({ token: token.token });
});Components
HookbasePortal
The provider component that wraps all Portal components.
tsx
<HookbasePortal
token={portalToken}
baseUrl="https://api.hookbase.app" // Optional
queryOptions={{
staleTime: 60000,
refetchOnWindowFocus: false,
}}
>
{children}
</HookbasePortal>EndpointList
Displays a list of the customer's webhook endpoints.
tsx
<EndpointList
onEndpointClick={(endpoint) => setSelected(endpoint)}
emptyMessage="No endpoints configured"
showStatus={true}
/>Props:
| Prop | Type | Description |
|---|---|---|
onEndpointClick | (endpoint) => void | Called when an endpoint is clicked |
emptyMessage | string | Message when no endpoints exist |
showStatus | boolean | Show endpoint status indicators |
EndpointForm
Form for creating new webhook endpoints.
tsx
<EndpointForm
onSuccess={(endpoint, secret) => {
// secret is only available on creation
alert(`Save this secret: ${secret}`);
}}
onError={(error) => console.error(error)}
defaultValues={{
url: 'https://',
description: '',
}}
/>Props:
| Prop | Type | Description |
|---|---|---|
onSuccess | (endpoint, secret?) => void | Called on successful creation |
onError | (error) => void | Called on error |
defaultValues | object | Default form values |
EndpointDetail
Shows details and management options for a single endpoint.
tsx
<EndpointDetail
endpointId={selectedEndpoint.id}
onDelete={() => setSelected(null)}
showSecret={false}
/>Props:
| Prop | Type | Description |
|---|---|---|
endpointId | string | The endpoint to display |
onDelete | () => void | Called after deletion |
showSecret | boolean | Allow viewing signing secret |
SubscriptionManager
Manages event type subscriptions for an endpoint.
tsx
<SubscriptionManager
endpointId={selectedEndpoint.id}
groupByCategory={true}
/>Props:
| Prop | Type | Description |
|---|---|---|
endpointId | string | The endpoint to manage |
groupByCategory | boolean | Group event types by category |
EventTypeList
Displays available event types (read-only).
tsx
<EventTypeList
groupByCategory={true}
showDescriptions={true}
/>MessageLog
Shows webhook delivery history.
tsx
<MessageLog
limit={25}
endpointId={selectedEndpoint?.id} // Optional filter
eventType="order.*" // Optional filter
refreshInterval={30000}
onMessageClick={(message) => showDetail(message)}
/>Props:
| Prop | Type | Description |
|---|---|---|
limit | number | Messages per page |
endpointId | string | Filter by endpoint |
eventType | string | Filter by event type (supports wildcards) |
refreshInterval | number | Auto-refresh interval in ms |
onMessageClick | (message) => void | Called when a message is clicked |
MessageDetail
Shows details of a specific message delivery.
tsx
<MessageDetail
messageId={selectedMessage.id}
showPayload={true}
showAttempts={true}
/>Styling
Default Styles
Import the default styles:
tsx
import '@hookbase/portal/styles.css';CSS Variables
Customize with CSS variables:
css
:root {
--hookbase-primary: #3b82f6;
--hookbase-primary-hover: #2563eb;
--hookbase-success: #22c55e;
--hookbase-error: #ef4444;
--hookbase-warning: #f59e0b;
--hookbase-border: #e5e7eb;
--hookbase-background: #ffffff;
--hookbase-text: #1f2937;
--hookbase-text-muted: #6b7280;
--hookbase-radius: 0.375rem;
}Tailwind CSS
Components use Tailwind-compatible class names. Override with your own classes:
tsx
<EndpointList className="my-custom-list" />Headless Mode
For complete control, use headless hooks:
tsx
import { useEndpoints, useMessages } from '@hookbase/portal';
function CustomEndpointList() {
const { data: endpoints, isLoading } = useEndpoints();
if (isLoading) return <MySpinner />;
return (
<MyList>
{endpoints.map(ep => (
<MyListItem key={ep.id}>{ep.url}</MyListItem>
))}
</MyList>
);
}Hooks
useEndpoints
tsx
const {
data: endpoints,
isLoading,
error,
refetch,
} = useEndpoints();useEndpoint
tsx
const {
data: endpoint,
isLoading,
} = useEndpoint(endpointId);useCreateEndpoint
tsx
const { mutate: createEndpoint, isPending } = useCreateEndpoint();
createEndpoint({
url: 'https://example.com/webhooks',
description: 'Production endpoint',
});useDeleteEndpoint
tsx
const { mutate: deleteEndpoint } = useDeleteEndpoint();
deleteEndpoint(endpointId);useRotateSecret
tsx
const { mutate: rotateSecret } = useRotateSecret();
const newSecret = await rotateSecret(endpointId);useEventTypes
tsx
const { data: eventTypes } = useEventTypes();useSubscriptions
tsx
const { data: subscriptions } = useSubscriptions(endpointId);useCreateSubscription
tsx
const { mutate: subscribe } = useCreateSubscription();
subscribe({ endpointId, eventTypeId });useDeleteSubscription
tsx
const { mutate: unsubscribe } = useDeleteSubscription();
unsubscribe(subscriptionId);useMessages
tsx
const { data, fetchNextPage, hasNextPage } = useMessages({
endpointId,
eventType: 'order.*',
limit: 25,
});useMessage
tsx
const { data: message } = useMessage(messageId);Full Example
tsx
import { useState, useEffect } from 'react';
import {
HookbasePortal,
EndpointList,
EndpointForm,
SubscriptionManager,
MessageLog,
} from '@hookbase/portal';
import '@hookbase/portal/styles.css';
export function WebhookDashboard() {
const [portalToken, setPortalToken] = useState<string | null>(null);
const [selectedEndpoint, setSelectedEndpoint] = useState(null);
const [showNewEndpoint, setShowNewEndpoint] = useState(false);
useEffect(() => {
fetch('/api/webhook-portal-token')
.then(res => res.json())
.then(data => setPortalToken(data.token));
}, []);
if (!portalToken) {
return <div>Loading...</div>;
}
return (
<HookbasePortal token={portalToken}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Endpoints Panel */}
<div>
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-semibold">Webhook Endpoints</h2>
<button
onClick={() => setShowNewEndpoint(true)}
className="btn btn-primary"
>
Add Endpoint
</button>
</div>
{showNewEndpoint && (
<EndpointForm
onSuccess={(endpoint, secret) => {
alert(`Endpoint created! Save this secret: ${secret}`);
setShowNewEndpoint(false);
setSelectedEndpoint(endpoint);
}}
onCancel={() => setShowNewEndpoint(false)}
/>
)}
<EndpointList
onEndpointClick={setSelectedEndpoint}
selectedId={selectedEndpoint?.id}
/>
</div>
{/* Subscriptions Panel */}
<div>
{selectedEndpoint ? (
<>
<h2 className="text-xl font-semibold mb-4">
Subscriptions for {selectedEndpoint.url}
</h2>
<SubscriptionManager
endpointId={selectedEndpoint.id}
groupByCategory={true}
/>
</>
) : (
<p className="text-gray-500">
Select an endpoint to manage subscriptions
</p>
)}
</div>
</div>
{/* Message Log */}
<div className="mt-8">
<h2 className="text-xl font-semibold mb-4">Recent Deliveries</h2>
<MessageLog
limit={25}
endpointId={selectedEndpoint?.id}
refreshInterval={30000}
/>
</div>
</HookbasePortal>
);
}Browser Support
- Chrome 80+
- Firefox 75+
- Safari 13+
- Edge 80+
Requires React 18 or later.