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