@proxies-sx/pool-portal-react
v0.5.0
Published
Drop-in React component and headless hooks for embedding a Proxies.sx Pool Gateway reseller dashboard into any React app.
Maintainers
Readme
@proxies-sx/pool-portal-react
Drop-in React component and headless hooks for embedding a Proxies.sx Pool Gateway reseller dashboard into any React app. Ships with a Next.js API route factory so you can wire it up in five minutes without reinventing the backend.
Install
npm install @proxies-sx/pool-portal-react @proxies-sx/pool-sdkPeer deps: React 18+, React-DOM 18+.
Optional default styles:
import '@proxies-sx/pool-portal-react/styles.css';5-minute quickstart (Next.js App Router)
1. Wire up the API route
Create app/api/pool/[...path]/route.ts:
import { createPoolApiHandlers } from '@proxies-sx/pool-portal-react/server';
import { ProxiesClient } from '@proxies-sx/pool-sdk';
import { auth } from '@/lib/auth'; // your auth lib (Clerk/NextAuth/…)
import { db } from '@/lib/db'; // your DB
export const { GET, POST } = createPoolApiHandlers({
proxies: new ProxiesClient({
apiKey: process.env.PROXIES_SX_API_KEY!,
proxyUsername: process.env.PROXIES_SX_USERNAME!,
}),
getSessionUserId: async () => (await auth())?.userId ?? null,
getUserKeyId: async (userId) => {
const customer = await db.customers.get(userId);
return customer?.pakKeyId ?? null;
},
});2. Drop the component into any page
// app/dashboard/page.tsx
import { PoolPortal } from '@proxies-sx/pool-portal-react';
import '@proxies-sx/pool-portal-react/styles.css';
export default function Dashboard() {
return (
<PoolPortal
apiRoute="/api/pool"
branding={{ name: 'AcmeProxies', primaryColor: '#6366f1' }}
/>
);
}That's the whole integration. The browser never sees your PROXIES_SX_API_KEY — all calls flow through your own API route.
How auth works
Customer's browser
│ (signed-in session via your auth lib)
▼
<PoolPortal apiRoute="/api/pool" />
│ fetch("/api/pool/me", { credentials: 'same-origin' })
▼
createPoolApiHandlers() (your /api/pool/[...path]/route.ts)
│ getSessionUserId() → who is this?
│ getUserKeyId() → which pak_ do they own?
│ proxies.poolKeys.list() → fetch usage from api.proxies.sx
▼
Respond with { proxyUsername, pakKey, usage }The component is strictly UI — it knows nothing about api.proxies.sx. Your API route is the trust boundary.
<PoolPortal> props
| Prop | Type | Default | Description |
|---|---|---|---|
| apiRoute | string | "/api/pool" | Base path of your mounted handlers |
| countries | Country[] | ['us','de','pl','fr','es','gb'] | Countries the dropdown offers |
| defaultCountry | Country | first in countries | |
| defaultProtocol | 'http' \| 'socks5' | 'http' | |
| defaultRotation | RotationMode | 'none' | |
| showStock | boolean | true | Show the live-endpoints indicator |
| showIncidents | boolean | true | Show an incident banner when active |
| showUsage | boolean | true | Show the usage bar |
| branding | Branding | — | { name, logoUrl, primaryColor, accentColor, radius, fontFamily } |
| classNames | PoolPortalClassNames | — | Per-part className overrides (Tailwind-friendly) |
| className | string | — | Extra class on the root |
| style | CSSProperties | — | Inline style on the root |
| emptyState | ReactNode | — | Rendered when the user has no key yet |
| onRegenerateKey | () => Promise<void> | — | Called when the user clicks "Regenerate key" |
Branding
<PoolPortal
branding={{
name: 'AcmeProxies',
logoUrl: '/logo.svg',
primaryColor: '#7c3aed',
accentColor: '#10b981',
radius: '12px',
fontFamily: '"Inter", sans-serif',
}}
/>Brand values map to CSS custom properties (--psx-primary, --psx-accent, --psx-radius, --psx-font). Skip styles.css and write your own CSS targeting these variables for total control.
Tailwind users
<PoolPortal
classNames={{
root: 'w-full max-w-2xl mx-auto',
card: 'bg-zinc-900 border-zinc-800 text-zinc-50',
button: 'bg-indigo-500 hover:bg-indigo-600',
usageBar: 'bg-zinc-800',
}}
/>Don't import styles.css and write everything in Tailwind.
Additional components (v0.4.0+ / v0.4.1+)
Compose with <PoolPortal> for full reseller-dashboard parity with client.proxies.sx/pool-proxy. All components self-contained, all honor the same branding / classNames / style props.
<PoolSessionSpawner> — multi-port URL generator (v0.4.0+)
<PoolSessionSpawner
proxyUsername={me.proxyUsername}
proxyPassword={me.pakKey}
countries={['us', 'de', 'gb', 'es', 'fr', 'pl']}
defaultPool="mbl"
defaultRotation="sticky"
defaultSessionType="unique" // unique-per-row sids → unique IPs
onSpawn={(urls) => analytics.track('proxy_spawn', { count: urls.length })}
/>Count slider (1–100), country / pool / protocol / rotation / sid-mode controls, "Generate" → N proxy URLs, per-row Copy + bulk Copy-all + Download .txt. The showTtlControl prop (v0.4.2 default true) exposes a "Session TTL override" field that appends -ttl-<seconds> to the username DSL (range 60-2592000 = 1 min to 30 days).
Also exports buildProxyString(opts) and defaultTtlSecondsForRotation(rotation) helpers for hand-rolled UIs.
<ActiveSessionsTable> — live session manager (v0.4.0+)
<ActiveSessionsTable
apiRoute="/api/pool"
proxyPassword={me.pakKey}
refreshIntervalMs={5_000}
onSessionClosed={(key) => toast.success(`Closed ${key.slice(-12)}`)}
/>Polls GET /api/pool/my-sessions (auto-handler) at 5 s default. Per-row: country, sid, IP, rotation, TTL countdown, bytes in/out, request count, Copy-URL (with password substitution), Close. Header Close-all + Refresh. Hides synthesized-sid sessions by default (hideSynthesizedSessions).
<PoolDocsPanel> — drop-in technical reference (v0.4.1+)
<PoolDocsPanel
proxyUsername={me.proxyUsername}
exampleSamplePassword={me.pakKey ?? '<YOUR_PASSWORD>'}
/>Four collapsible sections: how-it-works flow (5-step request lifecycle), username token reference (full DSL grammar), IP rotation modes (with TTL table), example curl (parametrized by your username). Pure presentational. Pass sections={['tokens', 'rotation']} to render only specific blocks.
<PoolStockGrid> — live country stock (v0.4.1+)
<PoolStockGrid
apiRoute="/api/pool"
countries={['us', 'de', 'gb', 'es', 'fr', 'pl']}
variant="grid" // or 'compact' for one-line-per-country
refreshIntervalMs={30_000}
/>Live online endpoint counts per country for both mbl mobile and peer residential pools. Auto-polls /api/pool/stock every 30 s (matches server-side cache TTL). Health pills: green ≥ 5 endpoints, amber < 5.
Server handlers (v0.4.0+)
createPoolApiHandlers() now exports a third method (DELETE) for the new session routes:
| Method | Path | Action |
|---|---|---|
| GET | <route>/my-sessions | List current user's sessions |
| DELETE | <route>/my-sessions/<sessionKey> | Close one (ownership-checked upstream) |
| DELETE | <route>/my-sessions | Close all for current user |
Make sure your route file exports all three: export const { GET, POST, DELETE } = createPoolApiHandlers({...}).
Headless hooks
Prefer to build your own UI? Use the same data layer:
import { usePoolKey, usePoolStock, useIncidents, buildProxyUrl } from '@proxies-sx/pool-portal-react';
function MyDashboard() {
const me = usePoolKey('/api/pool');
const stock = usePoolStock('/api/pool');
const incidents = useIncidents('/api/pool');
if (me.loading) return <Spinner />;
if (me.error || !me.data) return <ErrorView onRetry={me.refetch} />;
const url = buildProxyUrl(me.data.proxyUsername, me.data.pakKey, {
country: 'us',
rotation: 'sticky',
});
return <MyCustomUI url={url} usage={me.data.usage} stock={stock.data} />;
}All hooks return { data, loading, error, refetch }. usePoolStock and useIncidents poll every 30s/60s respectively; override with { refreshIntervalMs }.
Server API reference
createPoolApiHandlers(options) exposes these routes on whatever path you mount it:
| Method | Path | Auth | Response |
|---|---|---|---|
| GET | /me | required | MeResponse — proxy URL ingredients + usage |
| GET | /stock | public | Live endpoint counts per country |
| GET | /incidents | public | Active gateway incidents |
| POST | /regenerate | required | Rotates the user's pak_ key |
Options
| Option | Required | Description |
|---|---|---|
| proxies | ✅ | ProxiesClient instance |
| getSessionUserId(req) | ✅ | string \| null — who is making this request? |
| getUserKeyId(userId) | ✅ | string \| null — which pakKeyId belongs to this user? |
| gatewayHost | | Passed through to the browser for custom edge deployments |
| onAudit(event) | | Called on writes (e.g. regenerate). Log to your audit trail. |
Provisioning a key for a new customer
createPoolApiHandlers only reads existing keys. Create them server-side after a successful payment:
// app/api/stripe/webhook/route.ts
import { ProxiesClient } from '@proxies-sx/pool-sdk';
const proxies = new ProxiesClient({
apiKey: process.env.PROXIES_SX_API_KEY!,
proxyUsername: process.env.PROXIES_SX_USERNAME!,
});
// On `checkout.session.completed`:
const key = await proxies.poolKeys.create({
label: `customer:${session.customer}`,
trafficCapGB: Number(session.metadata?.gb),
// Optional: 60-day expiry. Top-ups extend it via poolKeys.topUp() (preferred over update).
expiresAt: new Date(Date.now() + 60 * 86_400_000).toISOString(),
idempotencyKey: `mint_${session.id}`, // SDK ≥ 0.3.0 — protect against double-mint on retry
});
await db.customers.update(customerId, { pakKeyId: key.id });
// On subsequent top-ups, prefer topUp() over update() — atomic + idempotent.
await proxies.poolKeys.topUp(key.id, {
addTrafficGB: 10, // server-side $inc, race-safe
extendDays: 60, // pushes expiresAt forward
idempotencyKey: `topup_${invoiceId}`,
});Time-bounded credits in the dashboard (v0.2.0+)
If you mint keys with expiresAt, surface it in your /api/pool/me response so <PoolPortal /> can render the countdown banner automatically:
// In your /me handler, return the key's expiresAt + isExpired
return NextResponse.json({
proxyUsername: process.env.PROXIES_SX_USERNAME!,
pakKey: key.key,
pakKeyId: key.id,
usage: {
usedMB: key.trafficUsedMB,
usedGB: (key.trafficUsedMB / 1024),
capGB: key.trafficCapGB,
enabled: key.enabled,
lastUsedAt: key.lastUsedAt,
expiresAt: key.expiresAt, // ISO string or null
isExpired: key.isExpired, // server-computed
},
});<PoolPortal /> will then render:
- > 7 days remaining → small dim line "Expires Aug 30, 2026 (88 days remaining)"
- ≤ 7 days → amber banner "Credits expire in N days. Top up to extend."
- Past expiry → red banner "Credits expired. Top up to reactivate."
Customers whose key has an expiry will see the countdown; those without an expiry see nothing extra.
Security
- Your
PROXIES_SX_API_KEYlives only on the server. The component never sees it. pak_keys are only sent to the customer they belong to (enforced by yourgetSessionUserId+getUserKeyId).- If a
pak_leaks, the user can hitPOST /api/pool/regenerate(wired toonRegenerateKey) to rotate it — the old value stops working immediately. /meresponses are sent withCache-Control: private, no-storeso they don't leak via CDN/browser cache.- Public endpoints (
/stock,/incidents) are cacheable (30s / 60s).
Runtime compatibility
- Works in any React 18+ environment: Next.js App/Pages Router, Vite, Remix, React Router
- Server handlers work on any runtime that supports standard
Request/Response(Node, Vercel Edge, Cloudflare Workers, Deno, Bun) - ESM + CJS +
.d.tstypes, zero runtime dependencies besides@proxies-sx/pool-sdk
License
MIT — see LICENSE.
