Skip to content

platform-esigning

Provider-agnostic e-signing infrastructure for NestJS. Exposes a unified EsigningClientPort abstraction with an internal provider out of the box and prepared entry points for DocuSign, Adobe Sign, Dropbox Sign, and signNow. Optionally tracks signing request state via the EsigningPersistencePort.

Package: @breadstone/archipel-platform-esigning

Architecture

Solid lines indicate shipped implementations. Dashed lines indicate provider entry points prepared for implementation. Each provider SDK is an optional peer dependency — install only the one you need.


Quick Start

1. Register with the internal provider

typescript
import { Module } from '@nestjs/common';
import { EsigningModule, InternalEsigningProvider } from '@breadstone/archipel-platform-esigning';

@Module({
  imports: [
    EsigningModule.register({
      provider: InternalEsigningProvider,
    }),
  ],
})
export class AppModule {}

2. Register with an external provider

typescript
import { Module } from '@nestjs/common';
import { EsigningModule } from '@breadstone/archipel-platform-esigning';
import { DOCUSIGN_CONFIG_ENTRIES } from '@breadstone/archipel-platform-esigning/docusign';

@Module({
  imports: [
    EsigningModule.register({
      provider: DocuSignClient, // your EsigningClientPort implementation
      configEntries: DOCUSIGN_CONFIG_ENTRIES,
      persistence: PrismaEsigningAdapter, // optional
      isGlobal: false,
    }),
  ],
})
export class AppModule {}

Switching providers

Replace the imports — no other code change needed:

typescript
import { ADOBE_SIGN_CONFIG_ENTRIES } from '@breadstone/archipel-platform-esigning/adobe-sign';

EsigningModule.register({
  provider: AdobeSignClient,
  configEntries: ADOBE_SIGN_CONFIG_ENTRIES,
});

Module Configuration

IEsigningModuleOptions

PropertyTypeRequiredDefaultDescription
providerType<EsigningClientPort>YesProvider implementation (e.g. InternalEsigningProvider)
configEntriesReadonlyArray<Omit<IConfigRegistryEntry, 'module'>>No[]Provider-specific config entries
persistenceType<EsigningPersistencePort>NoSigning request state tracking (e.g. Prisma adapter)
isGlobalbooleanNofalseRegister as a global module

E-Signing Providers

Internal (built-in)

The InternalEsigningProvider manages signing requests in-memory. Suitable for development, testing, or simple self-managed signing workflows.

typescript
import { InternalEsigningProvider } from '@breadstone/archipel-platform-esigning';

EsigningModule.register({
  provider: InternalEsigningProvider,
});

Internal Provider

The internal provider also exposes a completeSigner() method that allows the consuming application to programmatically complete a signer's step — useful for self-hosted signing UIs.

DocuSign

Subpath: @breadstone/archipel-platform-esigning/docusign

VariableDescriptionRequired
DOCUSIGN_INTEGRATION_KEYDocuSign integration key (client ID)Yes
DOCUSIGN_SECRET_KEYDocuSign secret key (client secret)Yes
DOCUSIGN_ACCOUNT_IDDocuSign account IDYes
DOCUSIGN_BASE_URLDocuSign base URL (e.g. https://demo.docusign.net)Yes
DOCUSIGN_WEBHOOK_HMAC_KEYDocuSign webhook HMAC keyNo
typescript
import { DOCUSIGN_CONFIG_ENTRIES } from '@breadstone/archipel-platform-esigning/docusign';

Adobe Sign

Subpath: @breadstone/archipel-platform-esigning/adobe-sign

VariableDescriptionRequired
ADOBE_SIGN_INTEGRATION_KEYAdobe Sign integration keyYes
ADOBE_SIGN_CLIENT_SECRETAdobe Sign client secretYes
ADOBE_SIGN_BASE_URLAdobe Sign base URLYes
ADOBE_SIGN_WEBHOOK_CLIENT_IDAdobe Sign webhook client ID for verificationNo
typescript
import { ADOBE_SIGN_CONFIG_ENTRIES } from '@breadstone/archipel-platform-esigning/adobe-sign';

Dropbox Sign

Subpath: @breadstone/archipel-platform-esigning/dropbox-sign

VariableDescriptionRequired
DROPBOX_SIGN_API_KEYDropbox Sign API keyYes
DROPBOX_SIGN_CLIENT_IDDropbox Sign client IDYes
DROPBOX_SIGN_WEBHOOK_SECRETDropbox Sign webhook secretNo
typescript
import { DROPBOX_SIGN_CONFIG_ENTRIES } from '@breadstone/archipel-platform-esigning/dropbox-sign';

signNow

Subpath: @breadstone/archipel-platform-esigning/signnow

VariableDescriptionRequired
SIGNNOW_API_KEYsignNow API key (basic token)Yes
SIGNNOW_CLIENT_IDsignNow client IDYes
SIGNNOW_CLIENT_SECRETsignNow client secretYes
SIGNNOW_BASE_URLsignNow base URLYes
SIGNNOW_WEBHOOK_SECRETsignNow webhook callback secretNo
typescript
import { SIGNNOW_CONFIG_ENTRIES } from '@breadstone/archipel-platform-esigning/signnow';

EsigningClientPort

All providers implement this abstract contract. Inject EsigningClientPort in services to stay provider-agnostic:

typescript
abstract class EsigningClientPort {
  abstract readonly providerId: string;
  abstract createSigningRequest(request: ICreateSigningRequest): Promise<ISigningRequest>;
  abstract createSigningSession(request: ICreateSigningSessionRequest): Promise<ISigningSession>;
  abstract getSigningRequest(signingRequestId: string): Promise<ISigningRequest>;
  abstract getSignedDocuments(signingRequestId: string): Promise<Array<ISignedDocument>>;
  abstract cancelSigningRequest(signingRequestId: string, reason?: string): Promise<void>;
  abstract verifyWebhook(payload: string | Buffer, signature: string): Promise<IEsigningWebhookEvent>;
}

Usage in services

typescript
import { EsigningClientPort } from '@breadstone/archipel-platform-esigning';

@Injectable()
export class ContractService {
  constructor(private readonly _esigning: EsigningClientPort) {}

  public async sendForSigning(contract: Contract): Promise<string> {
    const signingRequest = await this._esigning.createSigningRequest({
      subject: `Contract: ${contract.title}`,
      documents: [
        {
          name: 'contract.pdf',
          contentType: 'application/pdf',
          content: contract.pdfBuffer,
          order: 1,
        },
      ],
      signers: [
        {
          email: contract.signerEmail,
          name: contract.signerName,
          role: 'signer',
          order: 1,
        },
      ],
    });
    return signingRequest.signingRequestId;
  }
}

Create Signing Request

typescript
const signingRequest = await this._esigning.createSigningRequest({
  subject: 'NDA Agreement',
  message: 'Please sign this NDA at your earliest convenience.',
  documents: [{ name: 'nda.pdf', contentType: 'application/pdf', content: pdfBuffer, order: 1 }],
  signers: [{ email: 'alice@example.com', name: 'Alice', role: 'signer', order: 1 }],
  fields: [
    {
      type: 'signature',
      signerId: '...', // resolved after creation
      documentId: '...',
      pageNumber: 1,
      positionX: 100,
      positionY: 500,
      width: 200,
      height: 50,
      required: true,
    },
  ],
  expiresInHours: 72,
});
// → ISigningRequest

Create Signing Session

typescript
const session = await this._esigning.createSigningSession({
  signingRequestId: 'sr_abc123',
  signerId: 'sgn_xyz789',
  returnUrl: 'https://app.example.com/signing/complete',
});
// → ISigningSession { signingUrl, expiresAt, ... }

Get Signing Request Status

typescript
const request = await this._esigning.getSigningRequest('sr_abc123');
// → ISigningRequest { status, signers, documents, ... }

Get Signed Documents

typescript
const documents = await this._esigning.getSignedDocuments('sr_abc123');
// → ISignedDocument[] { documentId, content, contentType, sizeInBytes, ... }

Cancel Signing Request

typescript
await this._esigning.cancelSigningRequest('sr_abc123', 'Contract terms changed.');

Verify Webhook

typescript
const event = await this._esigning.verifyWebhook(rawBody, signatureHeader);
// → IEsigningWebhookEvent { eventType, signingRequestId, ... }

EsigningService

The EsigningService wraps EsigningClientPort and adds optional persistence tracking via EsigningPersistencePort:

typescript
import { EsigningService } from '@breadstone/archipel-platform-esigning';

@Injectable()
export class ContractWorkflowService {
  constructor(private readonly _esigning: EsigningService) {}

  public async initiateSigningFlow(contractId: string): Promise<ISigningRequest> {
    // Automatically persists via EsigningPersistencePort (if registered)
    return this._esigning.createSigningRequest({ ... });
  }
}

All methods on EsigningService mirror EsigningClientPort and additionally invoke the persistence port when registered:

MethodPersistence Hook
createSigningRequestonSigningRequestCreated()
cancelSigningRequestonSigningRequestCancelled()
verifyWebhookonSigningRequestStatusChanged()
createSigningSession
getSigningRequest
getSignedDocuments

Ports (Contracts)

EsigningPersistencePort

Tracks signing request state changes. Optional — when not provided, signing still works but state is not persisted.

typescript
abstract class EsigningPersistencePort {
  abstract onSigningRequestCreated(signingRequest: ISigningRequest): Promise<void>;
  abstract onSigningRequestStatusChanged(signingRequestId: string, status: string): Promise<void>;
  abstract onSigningRequestCancelled(signingRequestId: string): Promise<void>;
}

Real-World Adapter: Prisma

typescript
@Injectable()
export class PrismaEsigningAdapter extends EsigningPersistencePort {
  private readonly _prisma: PrismaService;

  constructor(prisma: PrismaService) {
    super();
    this._prisma = prisma;
  }

  public async onSigningRequestCreated(signingRequest: ISigningRequest): Promise<void> {
    await this._prisma.signingRequest.create({
      data: {
        externalId: signingRequest.signingRequestId,
        provider: 'docusign',
        subject: signingRequest.subject,
        status: signingRequest.status,
        createdAt: signingRequest.createdAt,
      },
    });
  }

  public async onSigningRequestStatusChanged(signingRequestId: string, status: string): Promise<void> {
    await this._prisma.signingRequest.update({
      where: { externalId: signingRequestId },
      data: { status, updatedAt: new Date() },
    });
  }

  public async onSigningRequestCancelled(signingRequestId: string): Promise<void> {
    await this._prisma.signingRequest.update({
      where: { externalId: signingRequestId },
      data: { status: 'cancelled', cancelledAt: new Date() },
    });
  }
}

Normalized Types

All providers map their native API responses into these shared interfaces.

ISigningRequest

FieldTypeDescription
signingRequestIdstringProvider-specific signing request identifier
statusSigningRequestStatusCurrent lifecycle status
subjectstringSubject or title
messagestring?Optional message for signers
signersReadonlyArray<ISigner>Participating signers
documentsReadonlyArray<ISigningDocument>Included documents
fieldsReadonlyArray<ISigningField>Fields across documents
createdAtDateCreation timestamp
updatedAtDateLast update timestamp
expiresAtDate?Expiration timestamp
providerMetadataRecord<string, unknown>?Provider-specific raw metadata

ISigner

FieldTypeDescription
signerIdstringProvider-specific signer identifier
emailstringEmail address
namestringDisplay name
roleSignerRoleRole in the signing process
statusSignerStatusCurrent status
ordernumberRouting order (signing sequence)

ISigningDocument

FieldTypeDescription
documentIdstringProvider-specific document identifier
namestringDisplay name
contentTypestringMIME type
sizeInBytesnumber?File size in bytes
pageCountnumber?Number of pages
ordernumberOrder within the signing request

ISigningField

FieldTypeDescription
fieldIdstringProvider-specific field identifier
typeSigningFieldTypeField type (signature, initials, etc.)
signerIdstringAssociated signer identifier
documentIdstringDocument this field is placed on
pageNumbernumberPage number (1-based)
positionXnumberX-coordinate in points
positionYnumberY-coordinate in points
widthnumberWidth in points
heightnumberHeight in points
requiredbooleanWhether the field is required
valuestring?Current value, if filled

ISignedDocument

FieldTypeDescription
documentIdstringProvider-specific document ID
signingRequestIdstringParent signing request ID
namestringDisplay name
contentTypestringMIME type
contentBufferSigned document content
sizeInBytesnumberFile size in bytes

ISigningSession

FieldTypeDescription
sessionIdstringSession identifier
signingRequestIdstringParent signing request ID
signerIdstringSigner this session belongs to
signingUrlstringURL to redirect signer to
expiresAtDateSession expiration timestamp

IEsigningWebhookEvent

FieldTypeDescription
eventIdstringProvider-specific event ID
eventTypeWebhookEventTypeNormalized event type
signingRequestIdstringRelated signing request ID
signerIdstring?Related signer ID (if applicable)
occurredAtDateEvent timestamp
rawPayloadunknownProvider-specific raw payload

Status & Role Enums

SigningRequestStatus

typescript
const SigningRequestStatuses = {
  Draft: 'draft',
  Sent: 'sent',
  InProgress: 'in-progress',
  Completed: 'completed',
  Declined: 'declined',
  Cancelled: 'cancelled',
  Expired: 'expired',
  Voided: 'voided',
} as const;

type SigningRequestStatus =
  | 'draft'
  | 'sent'
  | 'in-progress'
  | 'completed'
  | 'declined'
  | 'cancelled'
  | 'expired'
  | 'voided';

SignerStatus

typescript
const SignerStatuses = {
  Pending: 'pending',
  Sent: 'sent',
  Delivered: 'delivered',
  Signed: 'signed',
  Declined: 'declined',
  Expired: 'expired',
} as const;

type SignerStatus = 'pending' | 'sent' | 'delivered' | 'signed' | 'declined' | 'expired';

SignerRole

typescript
const SignerRoles = {
  Signer: 'signer',
  CarbonCopy: 'carbon-copy',
  InPersonSigner: 'in-person-signer',
  Approver: 'approver',
  Witness: 'witness',
} as const;

type SignerRole = 'signer' | 'carbon-copy' | 'in-person-signer' | 'approver' | 'witness';

SigningFieldType

typescript
const SigningFieldTypes = {
  Signature: 'signature',
  Initials: 'initials',
  DateSigned: 'date-signed',
  Text: 'text',
  Checkbox: 'checkbox',
  Name: 'name',
  Email: 'email',
  Company: 'company',
  Title: 'title',
} as const;

type SigningFieldType =
  | 'signature'
  | 'initials'
  | 'date-signed'
  | 'text'
  | 'checkbox'
  | 'name'
  | 'email'
  | 'company'
  | 'title';

WebhookEventType

typescript
const WebhookEventTypes = {
  SigningRequestCompleted: 'signing-request.completed',
  SigningRequestDeclined: 'signing-request.declined',
  SigningRequestVoided: 'signing-request.voided',
  SigningRequestExpired: 'signing-request.expired',
  SigningRequestSent: 'signing-request.sent',
  SignerCompleted: 'signer.completed',
  SignerDeclined: 'signer.declined',
  SignerSent: 'signer.sent',
  SignerDelivered: 'signer.delivered',
} as const;

Error Classes

ErrorCodeDescription
EsigningErrorCustom code propertyBase error for all e-signing failures
SigningProviderErrorSIGNING_PROVIDER_ERRORProvider-level operation failed
SigningRequestNotFoundErrorSIGNING_REQUEST_NOT_FOUNDSigning request not found
WebhookVerificationErrorWEBHOOK_VERIFICATION_FAILEDWebhook signature verification failed
typescript
import {
  EsigningError,
  SigningProviderError,
  SigningRequestNotFoundError,
  WebhookVerificationError,
} from '@breadstone/archipel-platform-esigning';

Implementing a Custom Provider

To implement a new e-signing provider, extend EsigningClientPort:

typescript
import { Injectable } from '@nestjs/common';
import { EsigningClientPort } from '@breadstone/archipel-platform-esigning';
import type {
  ICreateSigningRequest,
  ICreateSigningSessionRequest,
  IEsigningWebhookEvent,
  ISignedDocument,
  ISigningRequest,
  ISigningSession,
} from '@breadstone/archipel-platform-esigning';

@Injectable()
export class DocuSignClient extends EsigningClientPort {
  public readonly providerId = 'docusign';

  public async createSigningRequest(request: ICreateSigningRequest): Promise<ISigningRequest> {
    // DocuSign eSignature REST API implementation
  }

  public async createSigningSession(request: ICreateSigningSessionRequest): Promise<ISigningSession> {
    // Create recipient view URL
  }

  public async getSigningRequest(signingRequestId: string): Promise<ISigningRequest> {
    // Get envelope status
  }

  public async getSignedDocuments(signingRequestId: string): Promise<Array<ISignedDocument>> {
    // Download completed documents
  }

  public async cancelSigningRequest(signingRequestId: string, reason?: string): Promise<void> {
    // Void envelope
  }

  public async verifyWebhook(payload: string | Buffer, signature: string): Promise<IEsigningWebhookEvent> {
    // Verify HMAC and normalize Connect event
  }
}

// Register
EsigningModule.register({
  provider: DocuSignClient,
  configEntries: DOCUSIGN_CONFIG_ENTRIES,
});

Exports Summary

ExportTypeDescription
EsigningModuleNestJS ModuleMain module with register()
EsigningServiceServiceApplication service wrapping provider + persistence
EsigningClientPortPortAbstract provider contract
EsigningPersistencePortPortOptional state tracking contract
InternalEsigningProviderProviderBuilt-in in-memory implementation
IEsigningModuleOptionsInterfaceModule configuration options
ISigningRequestInterfaceSigning request model
ISigningSessionInterfaceSigning session/link model
ISignedDocumentInterfaceSigned document model
ISignerInterfaceSigner model
ISigningDocumentInterfaceDocument model
ISigningFieldInterfaceField definition model
IEsigningWebhookEventInterfaceNormalized webhook event
ICreateSigningRequestInterfaceSigning request creation input
ICreateSigningSessionRequestInterfaceSession creation input
SigningRequestStatusesConstantSigning request status values
SignerStatusesConstantSigner status values
SignerRolesConstantSigner role values
SigningFieldTypesConstantSigning field type values
WebhookEventTypesConstantWebhook event type values
EsigningErrorErrorBase error class
SigningProviderErrorErrorProvider-level error
SigningRequestNotFoundErrorErrorNot found error
WebhookVerificationErrorErrorWebhook verification error
ESIGNING_PROVIDERTokenDI token for provider
ESIGNING_PROVIDER_OPTIONSTokenDI token for provider options
ESIGNING_API_KEYConfig KeyGeneric API key config
ESIGNING_WEBHOOK_SECRETConfig KeyGeneric webhook secret config
ESIGNING_API_BASE_URLConfig KeyGeneric base URL config

Released under the MIT License.