@urblock/connect-server
A Fastify 5 plugin that exposes the full urblock Connect API — accounts, passkeys, session keys, relay, OAuth, recovery, and tenant settings — on your own infrastructure. Bring your own database, auth, and network config.
npm install @urblock/connect-server @urblock/connect-storage-kysely
When to use: you need full data sovereignty, custom auth, or want to embed Connect endpoints inside an existing Fastify service. For the managed version, use the SaaS API at
api.urblock.io.
Quick Start
import Fastify from "fastify";
import { Kysely, PostgresDialect } from "kysely";
import {
createConnectFastifyPlugin,
createStorageBackedConnectServices,
createStaticNetworkConfigAdapter,
createBearerTokenAuthAdapter,
RemoteOAuthIdentityVerifier,
HmacOAuthTokenCodec,
} from "@urblock/connect-server";
/* 1 — Database */
const db = new Kysely({ dialect: new PostgresDialect({ /* pg pool */ }) });
/* 2 — Network config (static or from DB) */
const networks = createStaticNetworkConfigAdapter([
{
chain_id: 137,
rpc_url: "https://polygon-rpc.com",
factory_address: "0xYourFactory",
bundler_url: "https://bundler.example.com",
session_key_validator_address: null,
},
]);
/* 3 — OAuth (optional) */
const oauthVerifier = new RemoteOAuthIdentityVerifier(
{ getOAuthClientIds: async () => ({ google: "YOUR_GOOGLE_ID", apple: null }) },
);
/* 4 — Bootstrap all services */
const runtime = createStorageBackedConnectServices({
db,
networkAdapter: networks,
oauthIdentityVerifier: oauthVerifier,
});
/* 5 — Auth adapter */
const auth = createBearerTokenAuthAdapter({
resolveContext: async (token) => {
// Look up the API key in your database
const key = await findApiKey(token);
if (!key) return null;
return { tenant_id: key.tenantId, key_type: key.type };
},
});
/* 6 — Register the plugin */
const app = Fastify();
await app.register(createConnectFastifyPlugin({ auth, services: runtime.services }));
await app.listen({ port: 3001 });
// → 42 routes available under /v1/connect/* and /v1/settings/*
Architecture
┌──────────────────────────────────────────────────┐
│ Fastify Server │
│ │
│ createConnectFastifyPlugin({ auth, services }) │
│ ▼ ▼ ▼ │
│ ┌─────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ Connect │ │ PendingOps │ │ OAuth │ │
│ │ Service │ │ Service │ │ Service │ │
│ └────┬─────┘ └──────┬──────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌────┴─────┐ ┌──────┴──────┐ ┌─────┴────────┐ │
│ │ Recovery │ │ Tenant │ │ Bundler │ │
│ │ Service │ │ Settings │ │ Client │ │
│ └────┬─────┘ └──────┬──────┘ └──────────────┘ │
│ │ │ │
│ ─────┴───────────────┴────────── Ports ───────── │
│ ▼ ▼ │
│ ConnectStorageRepository NetworkConfigAdapter │
│ TenantSettingsRepository OAuthIdentityVerifier │
└──────────────────────────────────────────────────┘
Bootstrap Functions
createStorageBackedConnectServices(options)
All-in-one bootstrap — creates repository instances from a Kysely db and wires every service.
interface CreateStorageBackedConnectServicesOptions {
db: Kysely<Database>;
networkAdapter: NetworkConfigAdapter;
oauthIdentityVerifier: OAuthIdentityVerifier;
oauthTokenCodec?: OAuthTokenCodec; // default: HmacOAuthTokenCodec
}
Returns a SelfHostedConnectServices object:
{
repositories: { connect, tenantSettings },
adapters: { networks, oauthIdentityVerifier, oauthTokenCodec },
services: { connect, pendingOps, oauth, recovery, tenantSettings },
}
createConnectServices(options)
Lower-level — you supply your own repository instances.
interface CreateConnectServicesOptions {
connectRepository: ConnectStorageRepository;
tenantSettingsRepository: TenantSettingsRepository;
networkAdapter: NetworkConfigAdapter;
oauthIdentityVerifier: OAuthIdentityVerifier;
oauthTokenCodec?: OAuthTokenCodec;
}
createStaticNetworkConfigAdapter(networks)
In-memory adapter for static chain configuration. Indexes by chain_id.
const adapter = createStaticNetworkConfigAdapter([
{
chain_id: 137,
rpc_url: "https://polygon-rpc.com",
factory_address: "0x...",
bundler_url: "https://bundler.example.com",
session_key_validator_address: null,
},
]);
createBearerTokenAuthAdapter(options)
Extracts the Bearer token from the Authorization header and delegates lookup to your callback.
const auth = createBearerTokenAuthAdapter({
resolveContext: async (token, request) => {
// Return ApiKeyContext or null (401)
return { tenant_id: "ten_abc", key_type: "secret" };
},
});
Services
ConnectServerService
Core smart-account operations.
| Method | Description |
|---|---|
createAccount(params) | CREATE2-deterministic smart account |
getAccount(address, chainId, tenantId) | Look up by address + chain |
listAccounts(tenantId, limit?, startingAfter?) | Paginated list |
verifySiwe(params) | SIWE authentication (ECDSA, P256, EIP-1271) |
registerPasskey(params) | Register a WebAuthn passkey |
deletePasskey(params) | Remove a passkey |
listPasskeysByAccount(address, tenantId) | List passkeys |
createSessionKey(params) | Create session key with module install calldata |
revokeSessionKey(params) | Revoke a session key |
listSessionKeysByAccount(address, tenantId) | List session keys |
relayUserOp(params) | Submit UserOp to the ERC-4337 bundler |
estimateUserOpGas(params) | Gas estimation |
getRelayOpStatus(userOpHash, tenantId) | Poll relay status |
getUserOpReceipt(params) | Fetch bundler receipt, updates status |
listRelayOpsByAccount(address, tenantId) | List relay ops |
encodeInstallModule(params) | Encode installModule calldata |
encodeUninstallModule(params) | Encode uninstallModule calldata |
encodeUninstallValidation(params) | Encode validation uninstall |
encodeChangeRootValidator(params) | Encode root validator change |
isModuleInstalled(params) | On-chain module check |
getNetworkConfig(chainId) | Network configuration |
ConnectOAuthService
Social login linking and verification.
| Method | Description |
|---|---|
verifyOAuth(params) | Verify Google/Apple ID token |
linkOAuth(params) | Link OAuth identity to account |
listOAuthLinks(address, tenantId, chainId) | List linked providers |
unlinkOAuth(address, provider, tenantId, chainId) | Remove link |
checkSafetyNet(address, tenantId, chainId) | Check recovery safety |
ConnectPendingOpsService
Multi-signature and deferred operation flow.
| Method | Description |
|---|---|
listPendingOps(tenantId) | List unsigned operations |
submitSignedOp(params) | Submit a signature for a pending op |
cancelPendingOp(userOpHash, tenantId) | Cancel a pending op |
ConnectRecoveryService
Social-recovery and account migration.
| Method | Description |
|---|---|
configureRecovery(params) | Set guardians + threshold + timelock |
initiateRecovery(params) | Start recovery process |
confirmRecovery(params) | Guardian confirms recovery |
executeRecovery(params) | Execute after timelock (≥ 48 h) |
cancelRecovery(params) | Cancel active request |
getRecoveryConfig(params) | Read current config |
listRecoveryRequests(params) | List recovery requests |
prepareMigration(params) | Encode fallback module install |
activateMigration(params) | Activate wallet mode change |
TenantSettingsService
Per-tenant configuration (connect, OAuth, passkeys, CORS).
| Method | Description |
|---|---|
getConnectSettings(apiKey) | Read connect settings |
updateConnectSettings(apiKey, body) | Update connect settings |
getOAuthSettings(apiKey) | Read OAuth client IDs |
updateOAuthSettings(apiKey, body) | Update OAuth client IDs |
getPasskeySettings(apiKey) | Read RP ID / name |
updatePasskeySettings(apiKey, body) | Update passkey RP config |
getCorsSettings(apiKey) | Read allowed origins |
updateCorsSettings(apiKey, body) | Update origins (max 20) |
Route Reference
All 42 routes registered by the plugin. Routes marked secret require a secret API key (sk_*); others accept publishable keys too.
Accounts
| Method | Path | Auth |
|---|---|---|
POST | /v1/connect/accounts | secret |
GET | /v1/connect/accounts | any |
GET | /v1/connect/accounts/:address | any |
SIWE Verification
| Method | Path | Auth |
|---|---|---|
POST | /v1/connect/verify | secret |
Passkeys
| Method | Path | Auth |
|---|---|---|
POST | /v1/connect/passkeys | secret |
GET | /v1/connect/accounts/:address/passkeys | any |
DELETE | /v1/connect/accounts/:address/passkeys/:credentialId | secret |
Session Keys
| Method | Path | Auth |
|---|---|---|
POST | /v1/connect/sessions | secret |
GET | /v1/connect/accounts/:address/sessions | any |
DELETE | /v1/connect/accounts/:address/sessions/:sessionKeyId | secret |
Relay (ERC-4337)
| Method | Path | Auth |
|---|---|---|
POST | /v1/connect/relay | secret |
GET | /v1/connect/relay/:userOpHash | any |
POST | /v1/connect/relay/estimate-gas | secret |
GET | /v1/connect/relay/:userOpHash/receipt | any |
GET | /v1/connect/accounts/:address/relay | any |
Modules & Validators
| Method | Path | Auth |
|---|---|---|
POST | /v1/connect/modules/install | secret |
POST | /v1/connect/modules/uninstall | secret |
POST | /v1/connect/validators/uninstall | secret |
POST | /v1/connect/validators/change-root | secret |
GET | /v1/connect/accounts/:address/modules/installed | any |
Networks
| Method | Path | Auth |
|---|---|---|
GET | /v1/connect/networks/:chainId | any |
Pending Operations
| Method | Path | Auth |
|---|---|---|
GET | /v1/connect/ops/pending | any |
POST | /v1/connect/ops/:userOpHash/sign | secret |
POST | /v1/connect/ops/:userOpHash/cancel | secret |
OAuth
| Method | Path | Auth |
|---|---|---|
POST | /v1/connect/oauth/verify | secret |
POST | /v1/connect/oauth/link | secret |
GET | /v1/connect/accounts/:address/oauth | any |
DELETE | /v1/connect/accounts/:address/oauth/:provider | secret |
GET | /v1/connect/accounts/:address/safety-net | any |
Recovery
| Method | Path | Auth |
|---|---|---|
GET | /v1/connect/accounts/:address/recovery | any |
POST | /v1/connect/recovery/configure | secret |
POST | /v1/connect/recovery/initiate | secret |
POST | /v1/connect/recovery/:requestId/confirm | secret |
POST | /v1/connect/recovery/:requestId/execute | secret |
POST | /v1/connect/recovery/:requestId/cancel | secret |
Migration
| Method | Path | Auth |
|---|---|---|
POST | /v1/connect/migrate/prepare | secret |
POST | /v1/connect/migrate/activate | secret |
Tenant Settings
| Method | Path | Auth |
|---|---|---|
GET | /v1/settings/connect | any |
PUT | /v1/settings/connect | secret |
GET | /v1/settings/oauth | any |
PUT | /v1/settings/oauth | secret |
GET | /v1/settings/passkeys | any |
PUT | /v1/settings/passkeys | secret |
GET | /v1/settings/cors | any |
PUT | /v1/settings/cors | secret |
Adapter Interfaces
These are the ports you implement to wire the server to your infrastructure.
NetworkConfigAdapter
interface NetworkConfig {
chain_id: number;
rpc_url: string;
factory_address: string | null;
bundler_url: string | null;
session_key_validator_address: string | null;
}
interface NetworkConfigAdapter {
getByChainId(chainId: number): Promise<NetworkConfig | null>;
}
Use createStaticNetworkConfigAdapter() for hardcoded chains, or implement the interface to load from a database.
ConnectHttpAuthAdapter
interface ApiKeyContext {
tenant_id: string;
key_type: "secret" | "publishable" | string;
environment?: "test" | "live" | string;
scopes?: string[];
}
interface ConnectHttpAuthAdapter {
resolveContext(request: FastifyRequest): Promise<ApiKeyContext> | ApiKeyContext;
assertSecretKey(context: ApiKeyContext): void;
}
Use createBearerTokenAuthAdapter() for Bearer-token auth, or implement the full interface for custom schemes.
OAuthIdentityVerifier
interface OAuthIdentityPayload {
iss: string; sub: string; aud: string | string[];
exp: number; iat: number;
email?: string; email_verified?: boolean;
name?: string; picture?: string;
}
interface OAuthIdentityVerifier {
verifyIdToken(params: {
tenantId: string;
provider: "google" | "apple";
idToken: string;
}): Promise<OAuthIdentityPayload>;
}
Use RemoteOAuthIdentityVerifier to verify tokens against Google/Apple JWKS endpoints, or provide a mock for testing.
OAuthTokenCodec
interface OAuthTokenCodec {
sign(payload: OAuthTokenCodecPayload): string;
verify(token: string): OAuthTokenCodecDecodedPayload | null;
}
The built-in HmacOAuthTokenCodec uses HMAC-SHA256 with a 5-minute TTL. Secret resolution (first match wins):
- Constructor
options.secret CONNECT_OAUTH_SECRETenv varAPI_SECRETenv varJWT_SECRETenv var"urblock-dev-secret"(dev-only fallback)
Error Handling
All errors are returned in a Stripe-style JSON envelope:
{
"error": {
"type": "invalid_request_error",
"code": "connect_account_not_found",
"message": "No connect account found.",
"param": "address",
"status": 404
}
}
Error types: invalid_request_error, authentication_error, authorization_error, api_error, rate_limit_error.
Throw ConnectServiceError in custom code to produce consistent errors:
import { ConnectServiceError } from "@urblock/connect-server";
throw new ConnectServiceError({
code: "custom_validation_error",
message: "Chain not supported",
status: 400,
param: "chain_id",
type: "invalid_request_error",
});
Storage Layer
The @urblock/connect-storage-kysely package provides ready-made PostgreSQL repositories:
import { ConnectStorageRepository, TenantSettingsRepository } from "@urblock/connect-storage-kysely";
const connectRepo = new ConnectStorageRepository(db);
const settingsRepo = new TenantSettingsRepository(db);
These implement all repository ports (ConnectRepositoryPort, ConnectOAuthRepositoryPort, ConnectPendingOpsRepositoryPort, RecoveryRepositoryPort, TenantSettingsRepositoryPort).
Bundler Client
Built-in ERC-4337 v0.7 bundler client (used internally by ConnectServerService):
import {
sendUserOperation,
estimateUserOperationGas,
getUserOperationReceipt,
} from "@urblock/connect-server";
const hash = await sendUserOperation(bundlerUrl, userOp);
const gas = await estimateUserOperationGas(bundlerUrl, userOp);
const receipt = await getUserOperationReceipt(bundlerUrl, hash);
Entry point: 0x0000000071727De22E5E9d8BAf0edAc6f37da032 (v0.7). Timeout: 15 s. Bundler errors map to status 502.
Environment Variables
| Variable | Default | Description |
|---|---|---|
CONNECT_OAUTH_SECRET | — | HMAC secret for OAuth token signing |
API_SECRET | — | Fallback HMAC secret |
JWT_SECRET | — | Second fallback HMAC secret |
All other configuration is injected via constructor parameters.