rdyrct

Encurtador de URL e gerador de QR de código aberto com links personalizados, análises de campanha, equipes, API e acesso MCP.

Documentação

rdyrct

Encurtador de links baseado em organizações + QR codes, rodando inteiramente na borda da Cloudflare.

License: MIT Built on Cloudflare Workers Bun React 19


Implante com um clique

Deploy to Cloudflare

Isso provisiona o Worker a partir do painel da Cloudflare. Ainda não é uma instalação completa por si só: você ainda precisa definir APP_URL, APP_HOST e os segredos, aplicar as migrações e anexar um domínio. Siga Implantar no Cloudflare Workers (rastreado em #280).


Recursos

  • Links curtos com slugs personalizados ou aleatórios, parâmetros UTM opcionais (source, medium, campaign, term, content) aplicados no momento do redirecionamento.
  • Endereços de link agrupados: renomear um link mantém o slug antigo redirecionando por 48 horas antes de ser liberado, e um link pode carregar aliases extras que todos redirecionam para o mesmo destino.
  • QR codes por link (planos pagos), com logotipo opcional no centro, pré-visualização ao vivo e download em PNG/SVG.
  • Domínios personalizados (Pro): conecte links.yourbrand.com via Cloudflare for SaaS, com slugs com namespace por domínio.
  • Organizações com funções owner / admin / member: administradores gerenciam a equipe, domínios e cobrança; membros gerenciam links.
  • Convites por magic-link: uso único, válidos por 7 dias, enviados por e-mail.
  • Análises que respeitam a privacidade: cliques por dia, principais links, países, referenciadores, dispositivos. Endereços IP nunca são armazenados.
  • Cobrança por usuário Free/Hobby/Pro através do Polar, que atua como Merchant of Record e lida com impostos/IVA.
  • Console de superadministrador fixado por segredo: a conta correspondente a SUPERADMIN_EMAIL recebe um painel de console na nuvem para uso, detalhamento de organizações e gerenciamento de planos. Ele retorna 404 para todos os outros.

Stack

rdyrct é construído Cloudflare-first: todo o produto roda em um único Worker sem backend separado para hospedar:

  • Cloudflare Workers: um Worker atende a API, o caminho quente de redirecionamento e o aplicativo React estático.
  • D1: SQLite na borda, fonte da verdade para autenticação, organizações, links, cliques e domínios.
  • KV: consultas slug → destino no caminho quente de redirecionamento, para que um clique nunca espere no D1.
  • R2: imagens de logotipo de QR, chaveadas por organização, servidas imutáveis e em cache.
  • Cloudflare for SaaS: hostnames personalizados para os próprios domínios de links curtos das organizações Pro.
  • Workers Rate Limiting: controles de abuso separados para autenticação, e-mail, gravações, uploads, domínios, cobrança e registro de cliques.

Camada de aplicação:


Desenvolvimento 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
  • Cadastre-se com o endereço que você definiu como SUPERADMIN_EMAIL para desbloquear o console de administrador; qualquer outro endereço é um usuário normal.

  • Novas contas veem um prompt de criação de organização em páginas com escopo de organização até criarem sua primeira organização.

  • Os códigos de verificação de e-mail são enviados através do emulador do Resend do emulate.dev em vez de uma caixa de entrada real. Leia com:

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

    e copie o código de 6 dígitos da mensagem mais recente.

  • Definir CF_DEV_ENV=simulated (o padrão .dev.vars.example) simula a API de Custom Hostnames da Cloudflare localmente: o DNS fica pronto após cerca de 3 segundos e o TLS após cerca de 12 segundos, então "Verificar status" mostra a mesma progressão sem uma zona real. CF_DEV_ENV=instant relata ambos prontos de uma vez, que é o que a execução e2e usa para não ficar esperando o temporizador. Sem nenhum dos dois, a falta de CF_API_TOKEN/CF_ZONE_ID falha de forma fechada em vez de fingir uma resposta.

  • A cobrança contra uma conta real do Polar sandbox precisa de uma URL pública (wrangler dev --remote ou um túnel) para que os webhooks alcancem /api/webhooks/polar.


Testes

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

O Playwright é dono do ciclo de vida do servidor local e nunca redefine ou semeia o estado local da Cloudflare. Testes de navegador com falha mantêm uma captura de tela e um trace em test-results/.


Implantar no Cloudflare Workers

Mínimo para rodar

  1. Um domínio na Cloudflare (ou o endereço gratuito *.workers.dev, para testar primeiro).
  2. Dois segredos: BETTER_AUTH_SECRET (qualquer string longa aleatória) e SUPERADMIN_EMAIL (a conta que se torna o administrador da plataforma).
  3. Os recursos abaixo, depois bun run db:migrate:remote && bun run deploy.

Todo o resto é opcional. Cada peça diz o que adiciona:

ConfiguraçãoAdiciona
RESEND_API_KEY, MAIL_FROME-mail: códigos de verificação de cadastro, redefinição de senha, convites, avisos. Sem isso, o cadastro pula a verificação (veja abaixo).
POLAR_*Venda dos planos Hobby e Pro. Sem isso, ninguém é limitado e a cobrança fica oculta.
GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET"Continuar com Google".
CF_API_TOKEN, CF_ZONE_IDDomínios próprios dos clientes através do Cloudflare for SaaS.
SHARED_LINK_HOSTUm segundo host mais curto para novos links de domínio compartilhado.
CAP_SECRETProva de trabalho na frente do cadastro e da redefinição de senha.
SENTRY_DSNRelatórios de erro para o seu projeto Sentry.

Sem provedor de e-mail e sem login do Google, as contas não são verificadas e as senhas não podem ser redefinidas. Isso é aceitável para uma instância privada e não recomendado para uma pública. O console de administrador avisa enquanto isso for verdade.

Criar os 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

Cole os ids retornados em wrangler.jsonc:

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

O nome do bucket R2 em wrangler.jsonc é rdyrct-qr-logos. Ou crie esse nome, ou altere r2_buckets[0].bucket_name para o que você criou.

Definir as variáveis e segredos

Em wrangler.jsonc, defina APP_URL (por exemplo https://links.example.com, sem barra final) e APP_HOST (links.example.com). O arquivo já vem com os valores de rdyrct.com para o resto (MAIL_FROM, POLAR_*, CF_ZONE_ID, SENTRY_DSN): limpe ou substitua cada um que você não usar.

Defina os segredos. Ou um por um:

bunx wrangler secret put BETTER_AUTH_SECRET
bunx wrangler secret put SUPERADMIN_EMAIL

ou em massa com cp prod.secrets.env.example prod.secrets.env, preencha e depois:

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

Então migre e envie:

bun run db:migrate:remote
bun run deploy

Anexe seu domínio

Painel da Cloudflare → Workers → seu worker → Settings → Domains & Routes → Add → Custom domain. Links curtos ficam na raiz (https://links.example.com/<slug>); o aplicativo é servido em todos os outros caminhos. Mantenha rotas fora de wrangler.jsonc: uma lista routes substitui o que estiver anexado no painel, e enviar a sua entregaria para cada fork. A rota */* é apenas para os domínios próprios dos clientes (abaixo).

E-mail (opcional)

Defina RESEND_API_KEY e MAIL_FROM (um remetente em um domínio que você verificou no Resend). Sem eles, nenhum e-mail sai: o cadastro pula o código de verificação e autentica o usuário, e o formulário de redefinição de senha diz que precisa de um provedor de e-mail. O envio vive em uma função, sendEmail() em src/worker/email.ts, então outro provedor é uma mudança ali.

Vender planos (opcional)

Pule esta seção para rodar sem limites e sem cobrança. Para vender planos, defina POLAR_SERVER=sandbox (ou production quando estiver ao vivo), POLAR_PRO_PRODUCT_ID e POLAR_HOBBY_PRODUCT_ID (produtos recorrentes que você cria no Polar), e os segredos POLAR_ACCESS_TOKEN e POLAR_WEBHOOK_SECRET.

Configuração do Polar: crie um Organization Access Token com escopos checkouts:write e customer_sessions:write. Adicione um endpoint de webhook em https://rdyrct.com/api/webhooks/polar e assine todos os cinco eventos que o Worker trata. Se faltar um, o estado de cobrança fica dessincronizado:

EventoO que faz
subscription.activeInicia a assinatura: plano, cliente, preço e acesso
subscription.updatedO snapshot completo da assinatura: status, produto, estado de cancelamento, fim do período
subscription.canceledMarca como terminando no fim do período; o acesso continua até lá
subscription.uncanceledLimpa um cancelamento agendado
subscription.revokedEncerra: a assinatura e seu acesso desaparecem

Quais status de assinatura mantêm acesso é uma decisão, em src/worker/entitlement.ts. active, trialing e past_due mantêm, porque o Polar tenta novamente um pagamento com falha por dias e envia subscription.revoked quando desiste. unpaid, canceled, incomplete e incomplete_expired não mantêm, e um status sem regra não mantém nada.

Acesso pago que um administrador concede manualmente é um comp, armazenado separado da assinatura e nunca contado como assinante. Um comp tem precedência sobre a assinatura abaixo dele, então revogar uma assinatura não pode tirar acesso que um administrador deu.

O console de administrador relata contagens, nunca dinheiro: quantas pessoas têm um plano pago, quantas pagam por ele e quantas são comped. Quem está cancelando aparece na própria linha na lista de usuários, onde nomeia alguém. A receita é lida no painel do Polar, que sabe o que cobrou líquido de descontos, impostos e reembolsos.

Segundo host de domínio compartilhado (opcional). Anexe um domínio mais curto da mesma forma, verifique que https://<it>/ responde com um redirecionamento, e só então ative com bunx wrangler secret put SHARED_LINK_HOST. Ele entra em vigor sem deploy, e bunx wrangler secret delete SHARED_LINK_HOST desliga. Nessa ordem porque cada novo link começa a usá-lo imediatamente: definido antes do domínio resolver, ele entrega links que não levam a lugar nenhum. Links antigos continuam funcionando em ambos os hosts, já que a chave KV de um slug de domínio compartilhado não tem host. Sem definir, links compartilhados usam APP_HOST.

Revise as políticas de rate limiting, monitoramento, regra de WAF e etapas de rollback antes do primeiro deploy de produção.

Revise a arquitetura, fluxos de dados e prioridades de melhoria antes de alterar fluxos de exclusão de link, domínio, logotipo ou organização.

Domínios personalizados do cliente (Pro): para permitir que organizações usem links.theirbrand.com, habilite Cloudflare for SaaS na zona rdyrct.com, crie uma origem fallback sem origem e com proxy (ex.: fallback.rdyrct.com AAAA 100::), e adicione uma rota de Worker */* apontando para o worker rdyrct. O CF_API_TOKEN precisa da permissão Zone → SSL and Certificates → Edit com escopo na zona. Também verifique seu domínio de envio (ex.: mail.rdyrct.com) no Resend para que e-mails transacionais não sejam bloqueados.


Configuração

NomeTipoFinalidade
BETTER_AUTH_SECRETsegredoAssina sessões e tokens
SUPERADMIN_EMAILsegredoA conta que se torna o administrador da plataforma
RESEND_API_KEYsegredoChave da API Resend para e-mail transacional
POLAR_ACCESS_TOKENsegredoToken de acesso à API Polar (escopos: checkouts:write, customer_sessions:write)
POLAR_WEBHOOK_SECRETsegredoVerifica assinaturas de webhooks do Polar; endpoint https://rdyrct.com/api/webhooks/polar
CF_API_TOKENsegredoToken Cloudflare com Zone → SSL and Certificates → Edit (domínios personalizados)
APP_URLvarURL pública completa do aplicativo, ex.: https://rdyrct.com
APP_HOSTvarHost público, ex.: rdyrct.com
SHARED_LINK_HOSTsegredo (opc.)Segundo host de domínio compartilhado para novos links, ex.: rdyr.cc; defina após ele resolver
MAIL_FROMvarEndereço de remetente para e-mail de saída
RESEND_BASE_URLvar (somente dev)Aponta para o emulador local do Resend
POLAR_SERVERvarsandbox ou production
POLAR_PRO_PRODUCT_IDvarID do produto Polar para o plano Pro
POLAR_HOBBY_PRODUCT_IDvarID do produto Polar para o plano Hobby
CF_ZONE_IDvarID da zona usada para Custom Hostnames
CF_DEV_ENVvar (somente dev)simulated simula a API de Custom Hostnames em um temporizador, instant sem um

Os segredos são definidos com wrangler secret put NAME ou wrangler secret bulk prod.secrets.env em produção; localmente, todos vêm de .dev.vars (veja .dev.vars.example).


Estrutura do projeto

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)

Licença

MIT © Andrea Bruno. Veja LICENSE.