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.
Despliega con un clic
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.comví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_EMAILobtiene 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:
- Hono para el enrutamiento
- BetterAuth para autenticación por correo+contraseña, con verificación por OTP por correo y hash PBKDF2/WebCrypto
- Drizzle ORM sobre D1
- React 19 + Vite +
@cloudflare/vite-plugin - Tailwind v4 + Base UI
- TanStack Query + React Router
- Polar para facturación, Resend para correo transaccional
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_EMAILpara 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=instantreporta 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 deCF_API_TOKEN/CF_ZONE_IDfalla 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 --remoteo 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
- Un dominio en Cloudflare (o la dirección gratuita de
*.workers.dev, para probarlo primero). - Dos secretos:
BETTER_AUTH_SECRET(cualquier cadena aleatoria larga) ySUPERADMIN_EMAIL(la cuenta que se convierte en administrador de la plataforma). - 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ón | Añade |
|---|---|
RESEND_API_KEY, MAIL_FROM | Correo 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_ID | Dominios propios de los clientes a través de Cloudflare for SaaS. |
SHARED_LINK_HOST | Un segundo host más corto para nuevos enlaces de dominio compartido. |
CAP_SECRET | Prueba de trabajo frente al registro y al restablecimiento de contraseña. |
SENTRY_DSN | Informes 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].idd1_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:
| Evento | Qué hace |
|---|---|
subscription.active | Inicia la suscripción: plan, cliente, precio y acceso |
subscription.updated | La instantánea completa de la suscripción: estado, producto, estado de cancelación, fin de período |
subscription.canceled | La marca como finalizando al fin del período; el acceso continúa hasta entonces |
subscription.uncanceled | Borra una cancelación programada |
subscription.revoked | La 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
| Nombre | Tipo | Propósito |
|---|---|---|
BETTER_AUTH_SECRET | secreto | Firma sesiones y tokens |
SUPERADMIN_EMAIL | secreto | La cuenta que se convierte en administradora de la plataforma |
RESEND_API_KEY | secreto | Clave API de Resend para correo transaccional |
POLAR_ACCESS_TOKEN | secreto | Token de acceso a la API de Polar (ámbitos: checkouts:write, customer_sessions:write) |
POLAR_WEBHOOK_SECRET | secreto | Verifica las firmas de los webhooks de Polar; endpoint https://rdyrct.com/api/webhooks/polar |
CF_API_TOKEN | secreto | Token de Cloudflare con Zona → SSL y Certificados → Editar (dominios personalizados) |
APP_URL | var | URL pública completa de la aplicación, p. ej. https://rdyrct.com |
APP_HOST | var | Host público, p. ej. rdyrct.com |
SHARED_LINK_HOST | secreto (opc.) | Segundo host de dominio compartido para nuevos enlaces, p. ej. rdyr.cc; configúralo después de que se resuelva |
MAIL_FROM | var | Dirección de remitente para el correo saliente |
RESEND_BASE_URL | var (solo dev) | Apunta al emulador local de Resend |
POLAR_SERVER | var | sandbox o production |
POLAR_PRO_PRODUCT_ID | var | ID de producto de Polar para el plan Pro |
POLAR_HOBBY_PRODUCT_ID | var | ID de producto de Polar para el plan Hobby |
CF_ZONE_ID | var | ID de zona utilizado para Custom Hostnames |
CF_DEV_ENV | var (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.