Portada de RepoAccess: un globo oscuro en el que las estelas de luz de los pagos convergen en un nodo hexagonal, con el texto Sell access to your private GitHub repos, your infrastructure, open-sourced core.

Cómo vender acceso a un repositorio privado de GitHub sin un SaaS en medio

Artículo de lanzamiento de RepoAccess: un Cloudflare Worker de código abierto que envía invitaciones a equipos de GitHub al cobrar y las revoca ante reembolsos y contracargos.

#cloudflare workers #github #payments #webhooks #stripe #open source #repoaccess

El plan gratuito de Polar se queda con el 5 % más 50 centavos de cada venta. Vende un boilerplate de 149 dólares cien veces y el contador marca 795 dólares.

Para ser justos, esa comisión compra una plataforma entera: checkout, impuestos, disputas. Pero si ya tienes un proveedor de pagos, la única pieza que te falta es la entrega: un webhook y una invitación a un equipo de GitHub por cada comprador. Ese hueco es lo que construí, y lo publiqué como código abierto.

Lo que hace falta de verdad para vender acceso a un repositorio privado de GitHub

La tarea suena trivial y no lo es. Al cobrar, hay que invitar la cuenta de GitHub del comprador al equipo que tiene el repositorio. RepoAccess lo hace sin OAuth: el comprador escribe su nombre de usuario de GitHub y acepta una invitación por correo, así que no hay ninguna app que autorizar en el momento más caro del embudo.

La otra cara es que un nombre de usuario puede escribirse mal, así que el comprador necesita una vía de corrección que no sea un hilo de soporte. Ante un reembolso o un contracargo, hay que retirar el acceso, siguiendo el calendario del proveedor y no el tuyo. Y todo esto tiene que ser idempotente, porque los proveedores de pagos reintentan los webhooks.

TL;DR

  • RepoAccess convierte webhooks de pago en invitaciones a equipos de GitHub; los reembolsos y contracargos revocan el acceso automáticamente
  • Funciona como un único Cloudflare Worker en el plan gratuito: sin servidor, sin suscripción SaaS, sin comisión por venta
  • Los compradores nunca autorizan una app OAuth; escriben un usuario de GitHub y aceptan una invitación por correo
  • El núcleo AGPL trae un adaptador de Stripe completo; otros proveedores se conectan a un contrato documentado

Mis propios productos se entregan como acceso a repositorios privados y no como descargas, y esta capa de entrega era la pieza que no podía comprar sin adoptar la plataforma de otro. Así que la construí como un único Cloudflare Worker, puse mis propios productos detrás y publiqué el núcleo como código abierto. Este artículo recorre la arquitectura: qué pasa cuando alguien paga, por qué es un worker y no un servicio, y dónde estuvieron de verdad las partes difíciles.

El stack, para que el código de abajo se lea sin adivinar: TypeScript, Hono 4 como router, Cloudflare Workers como runtime, Cloudflare Workflows para los pasos duraderos de concesión y revocación, y Workers KV para los tokens de reclamación y los registros de concesión. Sin base de datos, sin servicio de colas, sin más framework que eso. El núcleo es AGPL-3.0 y está en npm como repoaccess-core.

Qué pasa cuando alguien paga

Diagrama de flujo de RepoAccess: un proveedor de pagos como Stripe, Paddle o Lemon Squeezy envía un webhook a un único Cloudflare Worker en tu propia infraestructura, que lo convierte en una invitación a un equipo de GitHub.

Toda integración con un proveedor termina en la misma puerta: el proveedor hace un POST a /wh/<adapter> en tu worker. La ruta verifica antes de parsear. Para Stripe eso es una comprobación HMAC en tiempo constante sobre el cuerpo sin procesar, byte a byte, porque volver a serializar el JSON antes de verificar es la forma en que nacen los fallos de firma. Solo entonces el adaptador traduce la carga del proveedor a la única forma que habla el motor:

export interface NormalizedEvent {
  event_type: 'payment_success' | 'refund' | 'chargeback'
  product_id: string
  /** Stable correlation key - identical across an order and its later refund/chargeback. */
  transaction_id: string
  buyer_email: string | null
  github_username: string | null
  /** Refund events only: true=full, false=partial, null=n/a. */
  is_full_refund: boolean | null
}

Tres tipos de evento son todo el vocabulario. Todo lo demás que envía un proveedor es ruido que el adaptador filtra, y transaction_id es la clave de correlación: el reembolso que llega tres semanas después del pedido lleva el mismo identificador, y así es como la revocación encuentra su concesión.

El evento entra entonces en un Cloudflare Workflow, y el identificador de la instancia hace más trabajo del que parece:

const event = adapter.parse(raw)
if (!event) return c.text('unprocessable entity', 400)

// Deterministic Workflow id = the idempotency key. This IS the dedupe
// mechanism - no KV bookkeeping. A duplicate id is silently skipped.
const id = await workflowInstanceId(
  adapter.name,
  event.event_type,
  event.transaction_id,
  event.is_full_refund,
)
await c.env.ACCESS_WORKFLOW.createBatch([
  { id, params: { adapter: adapter.name, event, origin: 'webhook' } },
])
return c.text('ok', 200)

Los proveedores reintentan los webhooks, a veces segundos después, a veces cuando tu worker ya respondió 200. La solución habitual es una tabla de identificadores de evento ya vistos.

Este motor no mantiene esa tabla: el identificador de instancia se deriva del propio evento, así que un reintento construye el mismo identificador y el runtime de Workflows omite el duplicado. Idempotencia por construcción, no por contabilidad.

Y is_full_refund está en el identificador por una razón: un reembolso pagado en dos etapas tiene que convertirse en dos instancias, no en una deduplicada en silencio. Más sobre esto en la sección de las partes difíciles.

Dentro del Workflow, el trabajo duradero corre como pasos reintentables: asociar product_id a sus equipos de GitHub, llamar a GitHub, invitar al nombre de usuario. Un reembolso o un contracargo ejecuta el verbo contrario por la misma tubería, y la revocación es una reconciliación contra el estado real de GitHub y no contra un indicador guardado: una membresía que ya no existe se lee como inexistente, y el DELETE no hace nada. Esa propiedad es la que hace inofensivo cada reintento en este motor.

Diagrama de dos carriles de la revocación automática: un pago fluye por el worker hasta una invitación enviada, mientras un reembolso o contracargo fluye por el mismo worker hasta un acceso revocado, sin limpieza manual.

Cuando llega un pago sin un nombre de usuario de GitHub utilizable, porque no se escribió nada, el nombre es inválido o no existe en GitHub, la concesión no cae en un hilo de soporte. El worker genera un token de reclamación de un solo uso ligado a la transacción, y el comprador termina en una página de reclamación servida por el mismo worker: escribe el usuario, lo confirma y recibe la invitación.

El proyecto lo llama la ruta del error tipográfico, y es una red de seguridad, no un paso del flujo de ningún proveedor. El único caso que ningún sistema puede atrapar es un error que coincida con la cuenta real de otra persona; nada aguas abajo puede distinguirlo de una compra correcta. Por eso cada evento access.granted lleva la transacción y el nombre de usuario, y un ticket del tipo “pagué y no recibí nada” se resuelve de un vistazo.

Por qué un solo Cloudflare Worker y no una plataforma

Vuelve a la aritmética del principio. El porcentaje de una plataforma es un precio justo por el checkout, la gestión de impuestos y la responsabilidad en disputas, si necesitas eso. Quien ya tiene Stripe, o Paddle, o cualquier proveedor con webhooks, ya le está pagando a su procesador exactamente por eso.

Lo que queda es la entrega, y la entrega es pequeña. Una venta le cuesta al worker una petición de webhook, un puñado de operaciones en KV y una llamada a la API de GitHub.

El límite del plan gratuito más ajustado que toca es el de KV, 1.000 escrituras al día, y una venta son unas pocas escrituras. Así que el plan gratuito cobra cientos de ventas al día a cero, sin ningún servidor parado entre ellas.

La segunda razón es la identidad. No hay ningún “Login with GitHub” en el flujo: ni app OAuth que registrar, ni ruta de callback, ni almacén de sesiones, ni un secreto adicional que pueda caducar en silencio. El worker no guarda ninguna identidad del comprador; convierte un evento de pago en una invitación a un equipo, y esa es toda la relación.

Experiencia del comprador en tres pasos sin OAuth: pagar en el checkout de tu proveedor de pagos, indicar un usuario de GitHub en el checkout o mediante un enlace de reclamación de un solo uso, y aceptar la invitación al repositorio desde el correo.

La tercera razón es la confianza, y apunta hacia el código abierto, no en su contra. Para gestionar membresías de equipo, el worker necesita un token de GitHub con poder real sobre tu organización. Yo no le daría un token así a un binario cerrado de un desconocido, y no espero que tú lo hagas. Por eso el núcleo es AGPL-3.0 y cada línea entre el webhook y la llamada a GitHub está en el repositorio.

El 20 % difícil: firmas, reembolsos, idempotencia

El camino feliz de este producto es un proyecto de fin de semana, y no voy a fingir lo contrario. Lo que necesitó meses de ejecuciones reales para fiarme es todo lo que rodea al camino feliz: los casos que deciden si un desconocido consigue acceso gratis o si un comprador que pagó pierde el suyo.

Verifica los bytes que recibiste, no el JSON que parseaste

La verificación HMAC se hace sobre los bytes exactos que envió el proveedor. Parsea el cuerpo y vuelve a serializarlo, y la firma se rompe por el orden de las claves, por un espacio en blanco o por un escape unicode. Por eso el worker conserva el texto sin procesar y verifica antes de que nada toque JSON.parse.

La cabecera de firma de Stripe puede llevar varias firmas v1 a la vez, así funcionan las rotaciones de secreto, de modo que el verificador acepta cualquiera de ellas dentro de una ventana de repetición de 300 segundos, el valor por defecto de las propias librerías de Stripe. Y la comparación en sí no debe filtrar tiempos:

/**
 * Constant-time hex compare. The length check is acceptable: a digest's
 * length is fixed by its algorithm, so a mismatch only ever means an
 * invalid signature, not a secret-dependent branch.
 */
export function timingSafeEqualHex(a: string, b: string): boolean {
  if (a.length !== b.length) return false
  let diff = 0
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
  return diff === 0
}

Nada de esto es exótico. Todo esto es exactamente lo que una integración de camino feliz se salta, y saltárselo aquí significa que un webhook falsificado puede conceder acceso a tu repositorio.

El reembolso que llega por etapas

El mejor bug de la historia de este motor se encontró antes de que conociera a un comprador real. Los proveedores pueden reembolsar un pago por etapas, y el motor tiene una política full_refund_only: un reembolso parcial se procesa y, correctamente, se salta la revocación.

El identificador de Workflow de un reembolso era antes {adapter}-refund-{txn}. Así que cuando un evento posterior completaba el reembolso, llevaba la misma transacción, construía el mismo identificador, y la misma deduplicación que protege contra los reintentos se lo tragaba en silencio. Un comprador reembolsado por completo conservaba su acceso, que es justo el caso para el que existe esa política.

La solución vive en el propio identificador. El identificador de instancia de un reembolso lleva ahora su alcance, así que un reintento sigue deduplicándose, misma respuesta, mismo identificador, mientras que una compleción cambia partial por full, genera un identificador nuevo, y la revocación se ejecuta:

const suffix = eventType === 'refund' ? `-${refundScopeOf(isFullRefund)}` : ''
const readable = `${adapter}-${eventType}-${transactionId}${suffix}`

E isFullRefund pasó a ser un parámetro obligatorio en lugar de opcional, que es la verdadera propiedad de seguridad: un punto de encolado que olvide el alcance no produce en silencio un identificador con la forma antigua. No compila.

La lección va más allá de los reembolsos. Cuando tu clave de idempotencia se deriva del evento, la clave tiene que llevar cada respuesta que lleva el evento. Deduplica con menos, y dos respuestas distintas se funden en una.

Un agente lo despliega, y nunca ve tus secretos

El repositorio incluye un asistente de instalación, y su diseño invierte el patrón que usan la mayoría de las guías de instalación “listas para IA”. Toda herramienta de instalación fiable, wrangler login, gh auth login, create-next-app, funciona de una sola manera: la herramienta dirige y guarda el estado, y el humano responde. Una guía en prosa de la que un agente improvisa es lo contrario, y la prosa es una superficie sin probar.

Así que el asistente es un programa. El agente ejecuta un comando, npm run wizard:drive, muestra tal cual el registro que imprime y devuelve la respuesta. Nunca elige el siguiente paso, nunca compone un comando de shell y nunca diagnostica fuera del camino.

Un registro es una pregunta con un conjunto fijo de opciones, un campo de texto libre con nombre que se lee de un panel, o un paso manual que el humano confirma con la única palabra done. Y done se verifica, no se cree: allí donde el conductor puede comprobar el estado real, la organización, el equipo, el token, la URL del worker desplegado, lo hace, y una comprobación fallida devuelve a la pantalla dueña del dato incorrecto, con los modos de fallo conocidos adjuntos como datos. La ejecución termina con una compra de prueba real de extremo a extremo, así que “funciona” es una observación, no una esperanza.

La parte que más me importa es lo que el agente no puede ver. Los valores secretos los pegas tú mismo en .dev.vars; el despliegue le pasa ese archivo a wrangler, que lo lee directamente, así que los valores nunca pasan por el agente. Las reglas de permisos que le niegan al agente la lectura de los archivos de secretos, por su nombre, están confirmadas en el repositorio, y una prueba planta secretos falsos y demuestra la negativa. Puedes leer cómo se hace cumplir antes de ejecutar nada.

Ilustración dividida del asistente de instalación dirigido por un agente: una terminal de Claude Code u OpenCode junto a una bóveda de secretos, con archivos de claves que wrangler lee en un proceso hijo al que el agente no puede acceder.

Calcula alrededor de una hora de principio a fin, paneles incluidos. El asistente es independiente del agente y no necesita un modelo de frontera: el conductor es dueño de la secuencia y de la redacción, así que el agente solo muestra y transmite. Mis propias ejecuciones reales usaron Claude Code con Haiku, su nivel más barato, y OpenCode con un modelo gratuito incluido, como Big Pickle.

Qué incluye el núcleo AGPL y qué añade Pro

La división es por necesidad, no por mutilación. El núcleo en npm es el motor completo: el router de webhooks, el adaptador de Stripe, el Workflow de concesión y revocación, la página de reclamación, la ruta del error tipográfico y el asistente. Si vendes con Stripe y te va bien alojarlo tú mismo, el núcleo es el producto entero, y así se queda.

El núcleo también exporta cada primitiva necesaria para incrustar el servicio dentro de tu propio embudo en Cloudflare por RPC, sin ninguna ruta pública de webhook. Pro lo entrega como una clase de servicio lista y con soporte; el núcleo te da las piezas para montarlo tú.

Pro existe para dos vendedores: el que no tiene Stripe como proveedor y el que no quiere hacerse cargo del mantenimiento de los webhooks. Añade los otros cinco adaptadores, Paddle, Lemon Squeezy, Gumroad, Razorpay y Telegram Stars, incluidas dos opciones de Merchant of Record, Paddle y Lemon Squeezy, para quienes quieren que el proveedor cargue con los impuestos y el cumplimiento normativo. También incluye una tienda con temas y páginas para el comprador, y se mantiene: los proveedores cambian con el tiempo las cargas de sus webhooks, los nombres de sus eventos y sus esquemas de firma, y seguir esos cambios es el trabajo por el que de verdad pagas para no tener que hacerlo.

El precio es un pago único que incluye doce meses de actualizaciones y soporte. Si nunca renuevas, la versión que tienes sigue funcionando para siempre: tu clon y tu worker desplegado son tuyos, y nada llama a casa para comprobar nada.

La guía de decisión, dicha tan claro como puedo: Stripe más autoalojamiento, usa el núcleo. Cualquier otro proveedor, un Merchant of Record, o un embudo que prefieres que otro mantenga al día: para eso está Pro.

Qué se sacrifica a cambio

Sin OAuth, el nombre de usuario se escribe a mano, y lo escrito a mano puede estar mal. La cota importa más que el miedo: cada invitación cuesta una compra completada, así que nadie puede cultivar accesos, y lo que queda es el error de un solo comprador, una cuenta, una compra, revocable. La ruta del error tipográfico atrapa la mitad detectable; un error que coincida con la cuenta real de alguien se responde con el registro de eventos, no se detecta, y prefiero decirlo así antes que prometer una detección que no tengo.

El autoalojamiento es la otra mitad del trato. El worker corre en tu cuenta de Cloudflare contra tu organización de GitHub y el panel de tu proveedor, así que cuando algo está mal configurado, te lo dicen las comprobaciones del asistente y el flujo de eventos, pero no hay ninguna página de estado de un proveedor a la que señalar. Ser dueño del margen y ser dueño del busca son la misma decisión.

Y el plan gratuito tiene techo. Las 1.000 escrituras al día de KV se traducen en cientos de ventas al día, muy por encima del ritmo normal de un producto de código, pero no por encima de un pico el día del lanzamiento. El plan Workers Paid, por 5 dólares al mes, sube la cuota de escrituras a un millón al mes, y ahí deja de ser una restricción real.

Por dónde empezar

Si vendes con Stripe, clona repoaccess-core, ejecuta el asistente con el agente de código que ya uses y calcula alrededor de una hora. El README lleva la misma arquitectura que ha recorrido este artículo, con los detalles del cumplimiento confirmados junto al código.

Si tu proveedor es Paddle, Lemon Squeezy, Gumroad, Razorpay o Telegram Stars, o quieres que el mantenimiento corra por cuenta de otro, el nivel de pago está en la página del producto RepoAccess.

Un último hecho, porque es la afirmación más fuerte que puedo hacer sobre confiar en esta cosa: cada copia de Pro la entrega el propio RepoAccess. Pagas, un worker invita tu cuenta de GitHub al repositorio privado, y un reembolso retiraría la invitación. La capa de entrega se vende a sí misma de la misma forma en que vende todo lo demás.

Preguntas Frecuentes

¿Cómo se vende acceso a un repositorio privado de GitHub sin una plataforma SaaS en medio?

Ejecutando tú mismo la capa de entrega. RepoAccess es un Cloudflare Worker de código abierto (AGPL-3.0) desplegado en tu propia cuenta: tu proveedor de pagos envía un webhook por POST al worker, el worker verifica la firma sobre el cuerpo sin procesar, asocia el producto a un equipo de GitHub e invita al nombre de usuario del comprador. Un reembolso o un contracargo revoca el acceso por la misma tubería. El checkout se queda en el proveedor que ya usas; el adaptador de Stripe viene en el núcleo, y no hay comisión por venta además de las tarifas de tu propio procesador.

¿Cómo revoca RepoAccess el acceso a GitHub tras un reembolso o un contracargo?

Cada evento lleva un identificador de transacción estable, así que el reembolso que llega semanas después del pedido se correlaciona con su concesión. La revocación se ejecuta como un paso duradero de un Cloudflare Workflow y funciona por reconciliación contra el estado real de GitHub, no contra un indicador guardado: el comprador se elimina del equipo, y una membresía que ya no existe se lee como resuelta. Esa propiedad de reconciliación es lo que hace inofensivos los reintentos del proveedor.

¿Por qué RepoAccess no usa Login with GitHub (OAuth) para los compradores?

Porque elimina toda una clase de piezas móviles y un coste en el embudo. Sin OAuth no hay app que registrar, ni ruta de callback, ni almacén de sesiones, ni un secreto adicional que pueda caducar en silencio; el worker no guarda ninguna identidad del comprador. Los compradores escriben un nombre de usuario de GitHub y aceptan una invitación por correo, así que nada los interrumpe justo después de pagar, que es el momento más caro para añadir fricción.

¿Qué pasa si un comprador escribe mal su nombre de usuario de GitHub en el checkout?

Si el nombre no sirve, porque está vacío, es inválido o corresponde a una cuenta que no existe en GitHub, el worker genera un token de reclamación de un solo uso ligado a la transacción y el comprador termina en una página de reclamación: escribe el usuario, lo confirma y recibe la invitación. Un error que coincida con la cuenta real de otra persona no lo puede detectar ningún sistema, así que en lugar de prometer detección, cada evento access.granted lleva la transacción y el nombre de usuario, y un ticket de soporte se resuelve de un vistazo.

¿Cómo deduplica RepoAccess los reintentos de webhooks sin una base de datos?

El identificador de la instancia del Cloudflare Workflow se deriva del propio evento: adaptador, tipo de evento, identificador de transacción y, en los reembolsos, el alcance del reembolso. Un reintento del proveedor construye el mismo identificador y el runtime de Workflows omite el duplicado en silencio. No hay ninguna tabla de eventos ya vistos que mantener. El sufijo del alcance importa: un reembolso pagado por etapas pasa de parcial a total, genera un identificador nuevo, y la revocación se ejecuta en lugar de ser absorbida por la deduplicación.

¿Cuántas ventas al día aguanta RepoAccess en el plan gratuito de Cloudflare?

Cientos. El límite más ajustado del plan gratuito que toca el worker es Workers KV, con 1.000 escrituras al día, y una venta cuesta unas pocas escrituras; las peticiones tienen un tope mucho más alto, 100.000 al día. A partir de ahí, el plan Workers Paid, por 5 dólares al mes, sube la cuota de KV a un millón de escrituras mensuales, y el rendimiento deja de ser una restricción real para un producto de código.

¿Cuál es la diferencia entre el núcleo gratuito de RepoAccess y RepoAccess Pro?

El núcleo AGPL en npm es el motor completo para quien vende con Stripe: router de webhooks, adaptador de Stripe, Workflow de concesión y revocación, página de reclamación, asistente de instalación y las primitivas para incrustar el servicio en tu propio worker por RPC. Pro añade los otros cinco adaptadores, Paddle, Lemon Squeezy, Gumroad, Razorpay y Telegram Stars, incluidas dos opciones de Merchant of Record, además de una tienda con temas y mantenimiento y soporte continuos, como pago único con doce meses de actualizaciones.

¿Puede un agente de código desplegar RepoAccess por sí solo?

Sí, y por diseño, no por suerte. El asistente de instalación es una máquina de estados que el agente dirige con un solo comando: muestra cada registro tal cual, devuelve las respuestas, y cada done se verifica contra el estado real, organización, equipo, token, URL desplegada, con la recuperación dirigida a la pantalla dueña del dato incorrecto. Los secretos nunca pasan por el agente; wrangler lee el archivo de secretos por sí mismo, y las reglas de permisos que le niegan esa lectura al agente están confirmadas en el repositorio. Calcula alrededor de una hora, con un modelo barato: las ejecuciones reales usaron Claude Code con Haiku y OpenCode con un modelo gratuito incluido.

¿Necesito GitHub Pro o GitHub Team para vender acceso a un repositorio privado?

No. RepoAccess trabaja con equipos de una organización de GitHub, no con colaboradores de una cuenta personal, y una organización gratuita basta: los repositorios privados y los equipos están incluidos. Lo que necesitas es una organización que posea el repositorio que vendes, un equipo por producto y un token con permisos sobre esa organización. El límite que de verdad cuenta no es el número de colaboradores, sino la cuota de escrituras de Workers KV, y da para cientos de ventas al día.

3 de septiembre de 2026
← Volver al resumen

Utilizamos cookies para analizar el tráfico del sitio y mejorar su experiencia. Al hacer clic en "Aceptar todo", acepta nuestro uso de seguimiento analítico.