rdyrct

Acortador de URL y generador de códigos QR de código abierto con enlaces de marca, análisis de campañas, equipos, API y acceso MCP.

Documentación

rdyrct

Acortador de enlaces basado en organizaciones + códigos QR, ejecutándose completamente en el edge de Cloudflare.

License: MIT Built on Cloudflare Workers Bun React 19


Despliega con un clic

Deploy to Cloudflare

Esto aprovisiona el Worker desde el panel de Cloudflare. Aún no es una instalación completa por sí sola: todavía necesitas configurar APP_URL, APP_HOST y los secretos, aplicar las migraciones y adjuntar un dominio. Sigue Desplegar en Cloudflare Workers (seguido en #280).


Características

  • Enlaces cortos con slugs personalizados o aleatorios, parámetros UTM opcionales (source, medium, campaign, term, content) aplicados en el momento de la redirección.
  • Direcciones de enlace agrupadas: renombrar un enlace mantiene el slug antiguo redirigiendo durante 48 horas antes de liberarse, y un enlace puede llevar alias adicionales que todos redirigen al mismo destino.
  • Códigos QR por enlace (planes de pago), con un logo opcional en el centro, vista previa en vivo y descarga en PNG/SVG.
  • Dominios personalizados (Pro): conecta links.yourbrand.com vía Cloudflare for SaaS, con slugs con espacio de nombres por dominio.
  • Organizaciones con roles de owner / admin / member: los administradores gestionan el equipo, los dominios y la facturación; los miembros gestionan enlaces.
  • Invitaciones por magic-link: de un solo uso, válidas durante 7 días, enviadas por correo electrónico.
  • Analíticas respetuosas con la privacidad: clics por día, enlaces principales, países, referentes, dispositivos. Nunca se almacenan direcciones IP.
  • Facturación por usuario Free/Hobby/Pro a través de Polar, que actúa como Merchant of Record y gestiona impuestos/IVA.
  • Consola de superadministrador fijada por secreto: la cuenta que coincida con SUPERADMIN_EMAIL obtiene un panel de consola en la nube para uso, desglose de organizaciones y gestión de planes. Devuelve 404 para todos los demás.

Stack

rdyrct está construido Cloudflare-first: todo el producto se ejecuta en un único Worker sin backend separado que alojar:

  • Cloudflare Workers: un Worker sirve la API, la ruta rápida de redirección y la aplicación React estática.
  • D1: SQLite en el edge, fuente de verdad para autenticación, organizaciones, enlaces, clics y dominios.
  • KV: búsquedas slug → destino en la ruta rápida de redirección, para que un clic nunca espere en D1.
  • R2: imágenes de logos QR, con clave por organización, servidas inmutables y en caché.
  • Cloudflare for SaaS: hostnames personalizados para los dominios de enlaces cortos propios de las organizaciones Pro.
  • Workers Rate Limiting: controles de abuso separados para autenticación, correo electrónico, escrituras, subidas, dominios, facturación y registro de clics.

Capa de aplicación:


Desarrollo local

bun install
cp .dev.vars.example .dev.vars     # then edit; at minimum set SUPERADMIN_EMAIL

bunx emulate --service resend      # local Resend inbox on :4000

bun run db:migrate:local
bun run dev                        # https://rdyrct.localhost
  • Regístrate con la dirección que configures como SUPERADMIN_EMAIL para desbloquear la consola de administración; cualquier otra dirección es un usuario normal.

  • Las cuentas nuevas ven un aviso de crear organización en las páginas con ámbito de organización hasta que crean su primera organización.

  • Los códigos de verificación por correo se envían a través del emulador de Resend de emulate.dev en lugar de una bandeja de entrada real. Lée los con:

    curl localhost:4000/emails -H 'authorization: Bearer test_token_admin'
    

    y copia el código de 6 dígitos del último mensaje.

  • Configurar CF_DEV_ENV=simulated (el valor predeterminado de .dev.vars.example) simula la API de Cloudflare Custom Hostnames localmente: el DNS se vuelve listo después de unos 3 segundos y el TLS después de unos 12 segundos, por lo que "Check status" muestra la misma progresión sin una zona real. CF_DEV_ENV=instant reporta ambos listos a la vez, que es lo que usa la ejecución e2e para no esperar el temporizador. Sin ninguno de los dos, la falta de CF_API_TOKEN/CF_ZONE_ID falla de forma cerrada en lugar de simular una respuesta.

  • La facturación contra una cuenta real de Polar sandbox necesita una URL pública (wrangler dev --remote o un túnel) para que los webhooks lleguen a /api/webhooks/polar.


Pruebas

bun run test          # fast unit tests
bun run test:worker   # Worker routes with local D1 and KV bindings
bun run e2e:install   # install Chromium for Playwright once
bun run e2e:smoke     # real-browser baseline regression checks
bun run verify        # types, lint, formatting, unit, and Worker tests

Playwright controla el ciclo de vida del servidor local y nunca reinicia ni siembra el estado local de Cloudflare. Las pruebas de navegador fallidas guardan una captura de pantalla y un trace en test-results/.


Desplegar en Cloudflare Workers

Mínimo para ejecutar

  1. Un dominio en Cloudflare (o la dirección gratuita de *.workers.dev, para probarlo primero).
  2. Dos secretos: BETTER_AUTH_SECRET (cualquier cadena aleatoria larga) y SUPERADMIN_EMAIL (la cuenta que se convierte en administrador de la plataforma).
  3. Los recursos siguientes, y luego bun run db:migrate:remote && bun run deploy.

Todo lo demás es opcional. Cada pieza indica qué añade:

ConfiguraciónAñade
RESEND_API_KEY, MAIL_FROMCorreo electrónico: códigos de verificación de registro, restablecimiento de contraseña, invitaciones, avisos. Sin ello, el registro omite la verificación (ver abajo).
POLAR_*Venta de planes Hobby y Pro. Sin ello, nadie tiene límites y la facturación está oculta.
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET"Continuar con Google".
CF_API_TOKEN, CF_ZONE_IDDominios propios de los clientes a través de Cloudflare for SaaS.
SHARED_LINK_HOSTUn segundo host más corto para nuevos enlaces de dominio compartido.
CAP_SECRETPrueba de trabajo frente al registro y al restablecimiento de contraseña.
SENTRY_DSNInformes de errores a tu proyecto de Sentry.

Sin proveedor de correo y sin inicio de sesión con Google, las cuentas no se verifican y las contraseñas no se pueden restablecer. Eso está bien para una instancia privada y no se recomienda para una pública. La consola de administración lo indica mientras sea cierto.

Crear los recursos

bunx wrangler kv namespace create LINKS
bunx wrangler d1 create rdyrct
bunx wrangler r2 bucket create rdyrct-media
bunx wrangler queues create rdyrct-storage
bunx wrangler queues create rdyrct-storage-dlq

Pega los ids devueltos en wrangler.jsonc:

  • kv_namespaces[0].id
  • d1_databases[0].database_id

El nombre del bucket R2 en wrangler.jsonc es rdyrct-qr-logos. O crea ese nombre, o cambia r2_buckets[0].bucket_name al que hayas creado.

Configurar las variables y los secretos

En wrangler.jsonc, configura APP_URL (por ejemplo https://links.example.com, sin barra final) y APP_HOST (links.example.com). El archivo incluye los valores de rdyrct.com para el resto (MAIL_FROM, POLAR_*, CF_ZONE_ID, SENTRY_DSN): borra o reemplaza cada uno que no uses.

Configura los secretos. O uno por uno:

bunx wrangler secret put BETTER_AUTH_SECRET
bunx wrangler secret put SUPERADMIN_EMAIL

o en bloque con cp prod.secrets.env.example prod.secrets.env, rellénalo y luego:

bunx wrangler secret bulk prod.secrets.env && rm prod.secrets.env

Luego migra y despliega:

bun run db:migrate:remote
bun run deploy

Adjunta tu dominio

Panel de Cloudflare → Workers → tu worker → Settings → Domains & Routes → Add → Custom domain. Los enlaces cortos viven en la raíz (https://links.example.com/<slug>); la aplicación se sirve en cualquier otra ruta. Mantén las rutas fuera de wrangler.jsonc: una lista de routes reemplaza lo que esté adjunto en el panel, y comprometer la tuya la entregaría a cada fork. La ruta de */* es solo para los dominios propios de los clientes (abajo).

Correo electrónico (opcional)

Configura RESEND_API_KEY y MAIL_FROM (un remitente en un dominio que hayas verificado en Resend). Sin ellos no sale ningún correo: el registro omite el código de verificación e inicia sesión al usuario, y el formulario de restablecimiento de contraseña indica que necesita un proveedor de correo. El envío vive en una función, sendEmail() en src/worker/email.ts, por lo que otro proveedor es un cambio allí.

Vender planes (opcional)

Omite esta sección para ejecutar sin límites y sin facturación. Para vender planes, configura POLAR_SERVER=sandbox (o production en producción), POLAR_PRO_PRODUCT_ID y POLAR_HOBBY_PRODUCT_ID (productos recurrentes que crees en Polar), y los secretos POLAR_ACCESS_TOKEN y POLAR_WEBHOOK_SECRET.

Configuración de Polar: crea un Organization Access Token con los alcances checkouts:write y customer_sessions:write. Añade un endpoint de webhook en https://rdyrct.com/api/webhooks/polar y suscríbete a los cinco eventos que maneja el Worker. Si falta uno, el estado de facturación se desvía:

EventoQué hace
subscription.activeInicia la suscripción: plan, cliente, precio y acceso
subscription.updatedLa instantánea completa de la suscripción: estado, producto, estado de cancelación, fin de período
subscription.canceledLa marca como finalizando al fin del período; el acceso continúa hasta entonces
subscription.uncanceledBorra una cancelación programada
subscription.revokedLa termina: la suscripción y su acceso desaparecen

Qué estados de suscripción mantienen el acceso es una decisión, en src/worker/entitlement.ts. active, trialing y past_due lo mantienen, porque Polar reintenta un pago fallido durante días y envía subscription.revoked cuando se rinde. unpaid, canceled, incomplete y incomplete_expired no lo hacen, y un estado sin regla no mantiene nada.

El acceso de pago que un administrador otorga manualmente es un comp, almacenado aparte de la suscripción y nunca contado como suscriptor. Un comp supera a la suscripción que tiene debajo, por lo que revocar una suscripción no puede quitar el acceso que un administrador otorgó.

La consola de administración reporta conteos, nunca dinero: cuántas personas tienen un plan de pago, cuántas pagan por él y cuántas tienen comp. Quién está cancelando aparece en su propia fila en la lista de usuarios, donde nombra a alguien. Los ingresos se leen en el panel de Polar, que sabe lo que cobró neto de descuentos, impuestos y reembolsos.

Segundo host de dominio compartido (opcional). Adjunta un dominio más corto de la misma manera, verifica que https://<it>/ responda con una redirección, y solo entonces actívalo con bunx wrangler secret put SHARED_LINK_HOST. Tiene efecto sin un despliegue, y bunx wrangler secret delete SHARED_LINK_HOST lo desactiva. En ese orden porque cada nuevo enlace empieza a usarlo de inmediato: configurado antes de que el dominio resuelva, entrega enlaces que no llevan a ninguna parte. Los enlaces antiguos siguen funcionando en ambos hosts, ya que la clave KV de un slug de dominio compartido no tiene host. Sin configurar, los enlaces compartidos usan APP_HOST.

Revisa las políticas de limitación de tasa, monitoreo, regla WAF y pasos de reversión antes del primer despliegue de producción.

Revisa la arquitectura, flujos de datos y prioridades de mejora antes de cambiar los flujos de eliminación de enlaces, dominios, logos u organizaciones.

Dominios personalizados de clientes (Pro): para permitir que las organizaciones usen links.theirbrand.com, habilita Cloudflare for SaaS en la zona de rdyrct.com, crea un fallback origin proxied sin origen (por ejemplo fallback.rdyrct.com AAAA 100::), y añade una ruta de Worker */* apuntando al worker de rdyrct. El CF_API_TOKEN necesita el permiso Zone → SSL and Certificates → Edit con ámbito en la zona. También verifica tu dominio de envío (por ejemplo mail.rdyrct.com) en Resend para que el correo transaccional no sea bloqueado.


Configuración

NombreTipoPropósito
BETTER_AUTH_SECRETsecretoFirma sesiones y tokens
SUPERADMIN_EMAILsecretoLa cuenta que se convierte en administradora de la plataforma
RESEND_API_KEYsecretoClave API de Resend para correo transaccional
POLAR_ACCESS_TOKENsecretoToken de acceso a la API de Polar (ámbitos: checkouts:write, customer_sessions:write)
POLAR_WEBHOOK_SECRETsecretoVerifica las firmas de los webhooks de Polar; endpoint https://rdyrct.com/api/webhooks/polar
CF_API_TOKENsecretoToken de Cloudflare con Zona → SSL y Certificados → Editar (dominios personalizados)
APP_URLvarURL pública completa de la aplicación, p. ej. https://rdyrct.com
APP_HOSTvarHost público, p. ej. rdyrct.com
SHARED_LINK_HOSTsecreto (opc.)Segundo host de dominio compartido para nuevos enlaces, p. ej. rdyr.cc; configúralo después de que se resuelva
MAIL_FROMvarDirección de remitente para el correo saliente
RESEND_BASE_URLvar (solo dev)Apunta al emulador local de Resend
POLAR_SERVERvarsandbox o production
POLAR_PRO_PRODUCT_IDvarID de producto de Polar para el plan Pro
POLAR_HOBBY_PRODUCT_IDvarID de producto de Polar para el plan Hobby
CF_ZONE_IDvarID de zona utilizado para Custom Hostnames
CF_DEV_ENVvar (solo dev)simulated simula la API de Custom Hostnames con un temporizador, instant sin uno

Los secretos se configuran con wrangler secret put NAME o wrangler secret bulk prod.secrets.env en producción; localmente todos provienen de .dev.vars (ver .dev.vars.example).


Estructura del proyecto

migrations/            D1 schema (numbered SQL migrations, applied in order)
scripts/               Local dev utilities (e.g. seed-local.ts)
src/worker/            Hono API, BetterAuth, KV publishing, redirect hot path
  routes/              auth/user, orgs, links, qr-logos, domains, billing, admin
  better-auth.ts plan.ts util.ts guards.ts org-role.ts rate-limit.ts session.ts
  email.ts password.ts kv.ts storage.ts alerts.ts clicks.ts workflows.ts
src/shared/types.ts    DTOs + PLAN_LIMITS (shared worker ↔ app)
src/app/               React SPA
  routes/              page-level route components
  ui/                  design-system primitives
  components/          feature components
  lib/                 client utilities, API hooks
.agents/skills/        Agent skills (react-doctor)
.claude/skills/        Claude skills (fallow, react-doctor)

Licencia

MIT © Andrea Bruno. Ver LICENSE.