Skip to main content

@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.

MethodDescription
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.

MethodDescription
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.

MethodDescription
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.

MethodDescription
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).

MethodDescription
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

MethodPathAuth
POST/v1/connect/accountssecret
GET/v1/connect/accountsany
GET/v1/connect/accounts/:addressany

SIWE Verification

MethodPathAuth
POST/v1/connect/verifysecret

Passkeys

MethodPathAuth
POST/v1/connect/passkeyssecret
GET/v1/connect/accounts/:address/passkeysany
DELETE/v1/connect/accounts/:address/passkeys/:credentialIdsecret

Session Keys

MethodPathAuth
POST/v1/connect/sessionssecret
GET/v1/connect/accounts/:address/sessionsany
DELETE/v1/connect/accounts/:address/sessions/:sessionKeyIdsecret

Relay (ERC-4337)

MethodPathAuth
POST/v1/connect/relaysecret
GET/v1/connect/relay/:userOpHashany
POST/v1/connect/relay/estimate-gassecret
GET/v1/connect/relay/:userOpHash/receiptany
GET/v1/connect/accounts/:address/relayany

Modules & Validators

MethodPathAuth
POST/v1/connect/modules/installsecret
POST/v1/connect/modules/uninstallsecret
POST/v1/connect/validators/uninstallsecret
POST/v1/connect/validators/change-rootsecret
GET/v1/connect/accounts/:address/modules/installedany

Networks

MethodPathAuth
GET/v1/connect/networks/:chainIdany

Pending Operations

MethodPathAuth
GET/v1/connect/ops/pendingany
POST/v1/connect/ops/:userOpHash/signsecret
POST/v1/connect/ops/:userOpHash/cancelsecret

OAuth

MethodPathAuth
POST/v1/connect/oauth/verifysecret
POST/v1/connect/oauth/linksecret
GET/v1/connect/accounts/:address/oauthany
DELETE/v1/connect/accounts/:address/oauth/:providersecret
GET/v1/connect/accounts/:address/safety-netany

Recovery

MethodPathAuth
GET/v1/connect/accounts/:address/recoveryany
POST/v1/connect/recovery/configuresecret
POST/v1/connect/recovery/initiatesecret
POST/v1/connect/recovery/:requestId/confirmsecret
POST/v1/connect/recovery/:requestId/executesecret
POST/v1/connect/recovery/:requestId/cancelsecret

Migration

MethodPathAuth
POST/v1/connect/migrate/preparesecret
POST/v1/connect/migrate/activatesecret

Tenant Settings

MethodPathAuth
GET/v1/settings/connectany
PUT/v1/settings/connectsecret
GET/v1/settings/oauthany
PUT/v1/settings/oauthsecret
GET/v1/settings/passkeysany
PUT/v1/settings/passkeyssecret
GET/v1/settings/corsany
PUT/v1/settings/corssecret

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):

  1. Constructor options.secret
  2. CONNECT_OAUTH_SECRET env var
  3. API_SECRET env var
  4. JWT_SECRET env var
  5. "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

VariableDefaultDescription
CONNECT_OAUTH_SECRETHMAC secret for OAuth token signing
API_SECRETFallback HMAC secret
JWT_SECRETSecond fallback HMAC secret

All other configuration is injected via constructor parameters.