Manual implementation

Build VibeID sign-in from scratch.

Use this guide when you are building outside the official packages, auditing the protocol, or writing an integration for another framework.

Use packages when you can

For Next.js apps, start with @vibe-id/next. This page exists for custom servers, framework adapters, and developers who need the wire-level details.

Routes to implement

Your app needs a request endpoint, a callback endpoint, a browser status endpoint, and normal session endpoints.

MethodRoutePurpose
POST/api/vibe-id/requestCreate a short-lived challenge and return a deep link plus status URL.
POST/api/vibe-id/callbackReceive the VibeID app result and verify the returned signature.
GET/api/vibe-id/status/[requestId]Let the browser poll until the request is approved, rejected, failed, or expired.
GET/api/vibe-id/sessionRead the current browser session.
POST/api/vibe-id/logoutDelete the current browser session.

Challenge payload

VibeID signs one exact string. Store that exact string server-side and verify the returned signature over the same bytes.

signin.v1.<requestId>.<nonce>.<issuedAt36>.<expiresAt36>.<originBase64Url>
FieldRule
requestIdUnique server-generated id for this sign-in attempt.
nonceBase64url string with at least 96 bits of random entropy.
issuedAt36Unix timestamp in milliseconds, encoded in base36.
expiresAt36Unix timestamp in milliseconds, encoded in base36 and greater than issuedAt36.
originBase64UrlBase64url UTF-8 origin, normalized to scheme + host + optional port.

Deep link contract

Generate vibe-id://sign links. Short aliases keep QR codes compact; long names are accepted too.

const callbackUrl = new URL("/api/vibe-id/callback", siteUrl).toString();
const deepLinkUrl =
  `vibe-id://sign?p=${encodeURIComponent(challenge)}` +
  `&c=${encodeURIComponent(callbackUrl)}` +
  `&k=signin`;
NameAliasRequiredMeaning
payloadpyesThe exact payload to sign. For sign-in, this is the signin.v1 challenge.
callbackcyesThe HTTP(S) endpoint where VibeID posts the signed result.
kindkrecommendedUse signin for browser sign-in UX.
requestIdroptionalExplicit request id. If omitted, VibeID extracts it from signin.v1.
didHintdoptionalPreferred local DID. The user can still choose another local identity.
v-optionalProtocol version. Defaults to 1.

Callback payload

VibeID posts JSON to your callback endpoint. Treat successful callbacks as untrusted until the signature verifies.

{
  "status": "ok",
  "v": "1",
  "kind": "signin",
  "requestId": "abc123",
  "signature": "<base64>",
  "did": "did:vibe:p256:<base64url-compressed-public-key>",
  "alg": "P-256",
  "profile": {
    "displayName": "Personal",
    "initials": "P",
    "theme": {
      "key": "orbital",
      "displayName": "Orbital",
      "startColor": "#24335D",
      "accentColor": "#3468F7",
      "endColor": "#76D8F6",
      "avatarColor": "#84D8F4",
      "surfaceColor": "#EEF6FF",
      "surfaceAccentColor": "#D7E6FF"
    },
    "avatarUrl": null
  }
}
FieldMeaning
statusok or error.
vProtocol version. Current value is 1.
kindsignin for this flow.
requestIdThe request id from the challenge.
signatureBase64 signature. Required when status is ok.
diddid:vibe:p256:<base64url-compressed-public-key>. Required when status is ok.
algP-256.
profileOptional display metadata. Verify the signature before storing it.
errorMachine-readable error code when status is error.
messageOptional human-readable failure detail.
{
  "status": "error",
  "v": "1",
  "kind": "signin",
  "requestId": "abc123",
  "error": "user_rejected",
  "message": "The request was rejected on device."
}

Signature verification

The DID suffix is the base64url-encoded compressed 33-byte P-256 public key. Reconstruct the public key and verify the signature over the stored challenge.

  • Require alg: "P-256".
  • Decode did:vibe:p256:... as a compressed P-256 key.
  • Verify SHA-256 ECDSA over the exact stored challenge string.
  • Accept DER and IEEE P1363 ECDSA encodings during the pilot.
  • Reject missing, expired, or already completed requests before creating a browser session.

Browser session handoff

The callback is delivered by the mobile app, not the waiting browser. Store the verified result on the server, then let the browser polling endpoint set the HttpOnly session cookie when it observes success.

Profile metadata

A successful callback can include display metadata from the selected VibeID persona. This is not identity proof. Store it only after the DID signature verifies.

type VibeProfile = {
  displayName: string;
  initials: string;
  theme: {
    key: "orbital" | "nebula" | "flare" | "tidal";
    displayName: string;
    startColor: string;
    accentColor: string;
    endColor: string;
    avatarColor: string;
    surfaceColor: string;
    surfaceAccentColor: string;
  };
  avatarUrl: string | null;
};

Trim and length-limit text, accept only known theme keys and #RRGGBB colors, and only keep avatar URLs when they are HTTPS URLs controlled by VibeID. Older app versions can omit profile metadata, so keep a DID-derived fallback.