DEV Community

Cover image for Build a Partner-Readiness Gate for an Internal Mini-App Platform in TypeScript
FinClip Super-App
FinClip Super-App

Posted on

Build a Partner-Readiness Gate for an Internal Mini-App Platform in TypeScript

Build a Partner-Readiness Gate for an Internal Mini-App Platform in TypeScript

An internal mini-app pilot can answer useful engineering questions before a host app opens to third-party developers. It can show whether teams can release independently, whether capability permissions are enforceable, and whether the platform can stop or roll back one service without disturbing the others.

It cannot prove that external onboarding, contracts, support, or partner exits will work. That transition deserves an explicit gate rather than a meeting in which each stakeholder brings a different definition of “ready.”

This tutorial builds a small TypeScript policy module for that gate. The module does not automate a business decision. It collects evidence, enforces a few non-negotiable conditions, and produces an explainable recommendation. It also keeps internal delivery evidence separate from obligations that begin when an independent organisation joins the platform.

Model stages before metrics

A maturity score becomes confusing when nobody agrees on the stage being assessed. Start by defining the operating states:

export type PlatformStage =
  | "internal_pilot"
  | "multi_team"
  | "invited_partner"
  | "broader_ecosystem";

export type Decision = "hold" | "advance";
Enter fullscreen mode Exit fullscreen mode

Each transition answers a different question:

  • internal_pilot asks whether one bounded service can run safely and create a customer outcome.
  • multi_team asks whether another internal team can use the supported path without bespoke platform-team work.
  • invited_partner asks whether an independent organisation can onboard, operate, receive support, and exit under explicit rules.
  • broader_ecosystem asks whether those processes can support wider participation.

The code in this article focuses on the transition from multi_team to invited_partner. At that point, a platform should have credible internal evidence but may still lack external controls.

This stage model should live outside individual service repositories. A service team can provide evidence about its own ownership, permissions, release, and recovery behaviour, but it should not be able to redefine the platform's transition criteria. Keep the policy in a versioned governance repository or policy service, and record the version used for every decision.

It is also worth separating platform readiness from service acceptance. The platform may be ready to run an invited partner pilot while a particular partner service is unsuitable because it requests excessive data or has no support owner. Conversely, a carefully designed service cannot compensate for a platform that lacks independent credentials or removal controls. In practice, run two gates: one for the platform stage and another for every service release.

Store evidence, not opinions

A boolean named isReady tells reviewers very little. Readiness inputs should identify the evidence behind them. We can represent each check as an evidence item:

export interface EvidenceItem {
  passed: boolean;
  reference?: string;
  observedAt: string;
  owner: string;
}

export interface DeliveryMetrics {
  onboardingHours: number;
  manualInterventionRate: number;
  supportedPathAdoptionRate: number;
  changeFailureRate: number;
  medianRecoveryMinutes: number;
  documentationRequestsPerOnboarding: number;
}

export interface InternalEvidence {
  activeInternalTeams: number;
  customerOutcomeObserved: EvidenceItem;
  ownershipDeclared: EvidenceItem;
  capabilityAllowlistEnforced: EvidenceItem;
  dataPurposeDeclared: EvidenceItem;
  versionCompatibilityDefined: EvidenceItem;
  rollbackExercised: EvidenceItem;
  serviceRemovalExercised: EvidenceItem;
  incidentRouteTested: EvidenceItem;
  documentationUsedByNewTeam: EvidenceItem;
  unresolvedCriticalIncidents: number;
  metrics: DeliveryMetrics;
}

export interface ExternalControls {
  partnerDueDiligenceDefined: EvidenceItem;
  independentIdentityAndSigning: EvidenceItem;
  dataProcessingTermsApproved: EvidenceItem;
  partnerSupportModelApproved: EvidenceItem;
  commercialTermsApproved: EvidenceItem;
  terminationAndDataExitTested: EvidenceItem;
}

export interface ReadinessInput {
  currentStage: PlatformStage;
  internal: InternalEvidence;
  external: ExternalControls;
}
Enter fullscreen mode Exit fullscreen mode

The reference can point to an immutable report, release record, policy version, incident exercise, or signed approval. Avoid placing sensitive customer data in this object. The module needs proof that evidence exists, not the confidential contents of the evidence.

observedAt also matters. A rollback demonstration from two years ago may not say much about the current runtime. Production systems change, so a real implementation should apply freshness rules to selected items.

Evidence references should be resolvable by reviewers and durable enough to survive staff changes. A chat message saying that a test passed is weak evidence. A release record containing the runtime version, service version, approver, result, and timestamp is much stronger. The same principle applies to onboarding studies: capture where the new team needed help, which documentation page it used, and whether the platform team performed an undocumented action.

Some evidence belongs at platform level and some at service level. An identity isolation test may apply to the runtime as a whole. A declared data purpose belongs to a specific service and version. Mixing those scopes creates stale approvals when one service changes. Add fields such as scope, serviceId, serviceVersion, and runtimeVersion when moving this example toward production.

Finally, evidence can expire for more than technical reasons. A contract template may change, a regional privacy requirement may be updated, or the support team may reorganise. Assign a policy owner to each evidence type and let that owner define when revalidation is required.

Do not average away hard blockers

Weighted scores are convenient but dangerous when a high score can hide a missing ownership record or an untested exit. Some conditions should stop the transition regardless of the average.

export interface ReadinessDecision {
  decision: Decision;
  score: number;
  blockers: string[];
  warnings: string[];
  evidenceReferences: string[];
}

function requireEvidence(
  item: EvidenceItem,
  message: string,
  blockers: string[],
): void {
  if (!item.passed) blockers.push(message);
}

function collectReferences(items: EvidenceItem[]): string[] {
  return items
    .map((item) => item.reference)
    .filter((value): value is string => Boolean(value));
}
Enter fullscreen mode Exit fullscreen mode

Now define the hard blockers for the first invited partner:

function findBlockers(input: ReadinessInput): string[] {
  const blockers: string[] = [];
  const { internal, external } = input;

  if (input.currentStage !== "multi_team") {
    blockers.push("The platform must complete the multi-team stage first.");
  }

  if (internal.activeInternalTeams < 2) {
    blockers.push("At least two internal producer teams must use the platform.");
  }

  requireEvidence(
    internal.ownershipDeclared,
    "Every service needs a named operational owner.",
    blockers,
  );
  requireEvidence(
    internal.capabilityAllowlistEnforced,
    "Capability allowlists must be enforced by the runtime.",
    blockers,
  );
  requireEvidence(
    internal.rollbackExercised,
    "Rollback must be exercised, not merely documented.",
    blockers,
  );
  requireEvidence(
    internal.serviceRemovalExercised,
    "Service removal must be tested before partner onboarding.",
    blockers,
  );

  if (internal.unresolvedCriticalIncidents > 0) {
    blockers.push("Resolve critical platform incidents before opening further.");
  }

  requireEvidence(
    external.partnerDueDiligenceDefined,
    "Define partner due diligence and approval ownership.",
    blockers,
  );
  requireEvidence(
    external.independentIdentityAndSigning,
    "Provide independent partner identity, credentials, and signing.",
    blockers,
  );
  requireEvidence(
    external.dataProcessingTermsApproved,
    "Approve data-processing and permitted-purpose terms.",
    blockers,
  );
  requireEvidence(
    external.terminationAndDataExitTested,
    "Test partner termination and data-exit procedures.",
    blockers,
  );

  return blockers;
}
Enter fullscreen mode Exit fullscreen mode

These rules are examples. A healthcare, financial-services, or public-sector platform will have additional blockers. The useful property is their visibility: reviewers can challenge a rule, assign an owner, and version the policy rather than arguing over an unexplained score.

Score the evidence that supports judgement

Once blockers are separate, a score can summarise softer signals. It should never override a blocker.

function clamp(value: number, min = 0, max = 1): number {
  return Math.min(max, Math.max(min, value));
}

function scoreInternalDelivery(evidence: InternalEvidence): number {
  const { metrics } = evidence;

  const onboarding = 1 - clamp(metrics.onboardingHours / 80);
  const selfService = 1 - clamp(metrics.manualInterventionRate);
  const standardPath = clamp(metrics.supportedPathAdoptionRate);
  const stability = 1 - clamp(metrics.changeFailureRate / 0.25);
  const recovery = 1 - clamp(metrics.medianRecoveryMinutes / 240);
  const documentation =
    1 - clamp(metrics.documentationRequestsPerOnboarding / 12);

  return (
    onboarding * 0.15 +
    selfService * 0.25 +
    standardPath * 0.25 +
    stability * 0.15 +
    recovery * 0.10 +
    documentation * 0.10
  );
}
Enter fullscreen mode Exit fullscreen mode

The numbers are deliberately transparent. They are starting assumptions, not universal benchmarks. Teams should calibrate them against their own risk appetite and delivery history. For example, a four-hour recovery time may be unacceptable for a payment service and reasonable for a low-risk campaign tool.

The metrics also need interpretation. A low number of documentation questions could mean the docs are excellent, or that teams stopped trying. Combine delivery telemetry with interviews and observed onboarding sessions.

Calibrate by risk lane

One global threshold is rarely appropriate for every mini app. A read-only branch locator and a payment initiation service have different consequences. Add a risk classification before calculating readiness and let it choose the applicable policy profile.

A low-risk profile might accept a shorter observation period but still require ownership, capability allowlists, monitoring, and removal. A high-risk profile might require a recent recovery exercise, dual approval, stricter data controls, penetration-test evidence, and a lower tolerance for failed changes. The hard blockers should become stricter as consequence increases; they should not disappear because a service is expected to generate revenue.

Do not let service teams select the easiest lane without review. Risk classification should consider data sensitivity, financial impact, regulated activity, customer vulnerability, native capabilities, transaction reversibility, and the blast radius of an outage. Record the factors alongside the chosen lane so an auditor can understand the decision later.

Calibration is an operating exercise. Run the gate against several past releases and compare its result with what actually happened. If known problematic releases receive strong scores, inspect the missing evidence or poorly chosen threshold. If every release is blocked for the same administrative reason, decide whether the rule is essential, automatable, or written at the wrong scope. Keep the history; changing a threshold silently makes trend data misleading.

Produce an explainable result

The final assessor joins blockers, a threshold, and warnings:

const SCORE_THRESHOLD = 0.72;

export function assessPartnerReadiness(
  input: ReadinessInput,
): ReadinessDecision {
  const blockers = findBlockers(input);
  const score = scoreInternalDelivery(input.internal);
  const warnings: string[] = [];

  if (!input.internal.documentationUsedByNewTeam.passed) {
    warnings.push("Observe a new team onboarding from the published docs.");
  }

  if (!input.internal.customerOutcomeObserved.passed) {
    warnings.push("Confirm a customer outcome before expanding supply.");
  }

  if (!input.external.partnerSupportModelApproved.passed) {
    warnings.push("Approve escalation, incident, and complaint ownership.");
  }

  if (!input.external.commercialTermsApproved.passed) {
    warnings.push("Commercial terms remain open for the pilot.");
  }

  if (score < SCORE_THRESHOLD) {
    warnings.push(
      `Internal delivery score ${score.toFixed(2)} is below ${SCORE_THRESHOLD}.`,
    );
  }

  const evidenceReferences = collectReferences([
    ...Object.values(input.internal).filter(
      (value): value is EvidenceItem =>
        typeof value === "object" && value !== null && "passed" in value,
    ),
    ...Object.values(input.external),
  ]);

  return {
    decision:
      blockers.length === 0 && score >= SCORE_THRESHOLD
        ? "advance"
        : "hold",
    score,
    blockers,
    warnings,
    evidenceReferences,
  };
}
Enter fullscreen mode Exit fullscreen mode

A production version should validate runtime input with a schema library, persist the policy version with each decision, and log who approved the transition. It should also prevent a service owner from approving their own evidence where separation of duties is required.

The output should be presented as a decision record, not just a console value. Include the current and proposed stage, policy version, evidence snapshot, blockers, warnings, exceptions, approvers, and an expiry date. If an exception is granted, give it an owner and deadline. Permanent undocumented exceptions eventually become a second release path that is harder to secure and support.

Avoid exposing sensitive evidence references to every developer. The readiness service can return a stable evidence identifier and verification status while limiting the underlying report to authorised reviewers. The decision needs traceability without becoming a directory of security findings or partner contract locations.

Test the policy boundary

Policy code needs tests because small edits can change a governance decision. Vitest works well for a compact TypeScript example:

import { describe, expect, it } from "vitest";
import { assessPartnerReadiness, ReadinessInput } from "./readiness";

const passed = (reference: string) => ({
  passed: true,
  reference,
  observedAt: "2026-08-20T09:00:00Z",
  owner: "platform-risk@example.test",
});

function readyInput(): ReadinessInput {
  return {
    currentStage: "multi_team",
    internal: {
      activeInternalTeams: 3,
      customerOutcomeObserved: passed("report://customer-outcome/42"),
      ownershipDeclared: passed("registry://services/v18"),
      capabilityAllowlistEnforced: passed("test://runtime/allowlist/91"),
      dataPurposeDeclared: passed("registry://data-purpose/v12"),
      versionCompatibilityDefined: passed("policy://compatibility/v4"),
      rollbackExercised: passed("drill://rollback/2026-08"),
      serviceRemovalExercised: passed("drill://removal/2026-08"),
      incidentRouteTested: passed("drill://incident/2026-08"),
      documentationUsedByNewTeam: passed("study://onboarding/team-c"),
      unresolvedCriticalIncidents: 0,
      metrics: {
        onboardingHours: 18,
        manualInterventionRate: 0.08,
        supportedPathAdoptionRate: 0.94,
        changeFailureRate: 0.04,
        medianRecoveryMinutes: 28,
        documentationRequestsPerOnboarding: 2,
      },
    },
    external: {
      partnerDueDiligenceDefined: passed("policy://partners/v3"),
      independentIdentityAndSigning: passed("test://partner-iam/77"),
      dataProcessingTermsApproved: passed("legal://dpa/template-v5"),
      partnerSupportModelApproved: passed("runbook://partner-support/v2"),
      commercialTermsApproved: passed("legal://pilot-terms/v2"),
      terminationAndDataExitTested: passed("drill://partner-exit/2026-08"),
    },
  };
}

describe("partner readiness", () => {
  it("advances when evidence and delivery signals are sufficient", () => {
    const result = assessPartnerReadiness(readyInput());
    expect(result.decision).toBe("advance");
    expect(result.blockers).toEqual([]);
  });

  it("holds when rollback has not been exercised", () => {
    const input = readyInput();
    input.internal.rollbackExercised = {
      ...input.internal.rollbackExercised,
      passed: false,
    };

    const result = assessPartnerReadiness(input);
    expect(result.decision).toBe("hold");
    expect(result.blockers).toContain(
      "Rollback must be exercised, not merely documented.",
    );
  });

  it("does not let a strong score cancel a missing data agreement", () => {
    const input = readyInput();
    input.external.dataProcessingTermsApproved = {
      ...input.external.dataProcessingTermsApproved,
      passed: false,
    };

    const result = assessPartnerReadiness(input);
    expect(result.score).toBeGreaterThan(0.72);
    expect(result.decision).toBe("hold");
  });
});
Enter fullscreen mode Exit fullscreen mode

The last test captures the most important design choice: excellent internal delivery performance cannot compensate for a missing external obligation.

Run the gate as a process

Code alone does not create governance. Connect it to a small operating routine:

  1. Service and platform owners attach evidence references.
  2. Security, privacy, operations, legal, and product owners review the checks they own.
  3. The assessor runs against a versioned policy.
  4. Reviewers discuss blockers and warnings separately.
  5. The decision, exceptions, expiry date, and approvers are recorded.
  6. A limited partner pilot is reassessed after its first release and exit exercise.

Reassessing matters because partner readiness is not permanent. Documentation, runtime versions, support arrangements, and regulations change. A gate should be triggered by meaningful platform changes as well as calendar intervals.

The invited pilot itself should generate new evidence. Track the time a partner spends waiting for access, the number of undocumented questions, support handoffs, rejected capability requests, integration defects, and any manual steps performed by employees. Run a suspension and exit exercise before declaring the pilot complete. A successful first release shows that the service can launch; an orderly exit shows that the host can retain control when the relationship changes.

Keep the pilot deliberately narrow. Limit the first partner's capability set, audience, data access, and transaction types. Feature flags and independent kill controls make this boundary enforceable. Expansion can follow after the team has reviewed production behaviour and closed the gaps found during onboarding.

What this gate tells you

An internal mini-app programme is a strong source of operational evidence. It can demonstrate repeatable onboarding, scoped capabilities, release safety, and customer value. The partner gate prevents that evidence from being stretched beyond what it supports.

The resulting decision is intentionally modest. It says whether the platform has met the organisation's current conditions for an invited partner pilot. It does not predict marketplace demand or guarantee a successful ecosystem.

That modesty is useful. Teams can see why a decision was made, which conditions are mandatory, and what work remains. The first external partner then enters a platform with a tested delivery path and a clear list of new obligations, rather than becoming the experiment that discovers both at once.

Top comments (0)