TURFY

Conquiste seu bairro. Um passo de cada vez.

O que e o Turfy? A ideia em 30 segundos

graph LR
    A["Voce corre
no mundo real"] --> B["Trail
rastro no mapa"] B --> C{"Trail
fechou loop?"} C -->|Sim| D["Territorio!
Area e sua"] C -->|Nao| B D --> E["Outros jogadores
podem disputar"] E --> F["Quem cercou
mais vezes domina"] style A fill:#1a1a2e,stroke:#00d2ff,color:#fff style B fill:#1a1a2e,stroke:#7b2ff7,color:#fff style C fill:#2a1a3e,stroke:#a78bfa,color:#fff style D fill:#1a2e1a,stroke:#4ade80,color:#fff style E fill:#2e1a1a,stroke:#ff6b6b,color:#fff style F fill:#2e2a1a,stroke:#facc15,color:#fff

Inspiracao

  • Strava tracking
  • Snake (jogo da cobrinha) trail
  • Territorial.io dominio
  • Pokemon GO mundo real

Publico

  • Corredores que querem gamificacao
  • Caminhantes casuais
  • Gamers de jogos com localizacao
  • Comunidades fitness competitivas

Gameplay Como funciona o jogo

O jogador corre, o trail cresce atras dele como uma cobra. Quando o trail cruza a si mesmo, a area dentro do loop e conquistada.

sequenceDiagram
    participant J as Jogador
    participant A as App (Mobile)
    participant S as Servidor

    J->>A: Toca "Iniciar Corrida"
    activate A
    A->>A: Inicia GPS + sensores
    Note over A: Trail cresce a cada passo

    loop A cada ponto GPS
        A->>A: Adiciona ponto ao trail
        A->>A: Verifica intersecao do trail consigo mesmo
    end

    A->>A: Loop detectado! Area = 2.500m2
    A-->>J: Feedback visual + som
    A->>S: Envia poligono + dados de sensor
    S->>S: Calcula Confidence Score (87/100)
    S->>S: PostGIS: verifica intersecoes
    S-->>A: Territorio aprovado!
    A-->>J: Territorio aparece no mapa
    S-->>S: Broadcast WebSocket para jogadores proximos
    deactivate A
            

Sistema de Camadas (Stacking)

Quanto mais vezes voce cerca uma area, mais forte o dominio. Outros jogadores precisam cercar MAIS vezes para tomar.

graph TD
    subgraph "Andrei cerca 5x"
        A1["Volta 1: +1 camada"] --> A2["Volta 2: +1 camada"]
        A2 --> A3["Volta 3: +1 camada"]
        A3 --> A4["Volta 4: +1 camada"]
        A4 --> A5["Volta 5: +1 camada
= 5 camadas total"] end subgraph "Maria tenta tomar" M1["Maria cerca 1x: 1 < 5"] -->|"Nao domina"| M2["Maria cerca 3x: 3 < 5"] M2 -->|"Nao domina"| M3["Maria cerca 5x: 5 = 5"] M3 -->|"Empate - Andrei mantem"| M4["Maria cerca 6x: 6 > 5"] M4 -->|"Maria domina!"| M5["Territorio e da Maria
com 1 camada de vantagem"] end A5 -.->|"Maria disputa"| M1 style A5 fill:#1a2e1a,stroke:#4ade80,color:#fff style M5 fill:#2e1a2e,stroke:#ff6b6b,color:#fff

Arquitetura Geral Visao macro do sistema

Mobile
Backend
Database
External
Infra
graph TB
    subgraph MOBILE["MOBILE (React Native + Expo)"]
        direction TB
        UI["UI Layer
expo-router + Zustand"] MAP["Mapa
@rnmapbox/maps"] GPS["GPS Tracking
expo-location"] SENSORS["Sensores
Acelerometro + Giroscopio"] LOOP["Loop Detection
@turf/turf"] CACHE["Cache Local
MMKV + React Query"] end subgraph CF["CLOUDFLARE (tudo num lugar)"] direction TB ROUTER["Workers
Hono + tRPC"] AUTH["Auth Middleware
Clerk"] ANTICHEAT["Anti-Cheat Engine
Confidence Score"] GEO["Geo Processing
PostGIS queries"] WS["Durable Objects
WebSocket Rooms
por Geohash"] JOBS["Queues
Decay, Rankings,
Region Merge"] HYPER["Hyperdrive
DB Connection Pool"] R2["R2
Object Storage"] end subgraph DATA["DATABASES"] direction TB PG["PostgreSQL + PostGIS
(Neon)"] REDIS["Redis (Upstash)
Cache only"] end subgraph EXTERNAL["SERVICOS EXTERNOS"] CLERK["Clerk
Auth"] MAPBOX["MapBox
Tiles + Geocoding"] SENTRY["Sentry
Monitoring"] end UI --> MAP UI --> GPS UI --> SENSORS GPS --> LOOP SENSORS --> LOOP LOOP --> CACHE CACHE <--> ROUTER MAP <--> MAPBOX UI <--> AUTH AUTH <--> CLERK ROUTER --> ANTICHEAT ANTICHEAT --> GEO GEO <--> HYPER HYPER <--> PG ROUTER <--> WS ROUTER --> JOBS JOBS <--> HYPER ROUTER --> R2 ROUTER --> SENTRY ROUTER <--> REDIS style MOBILE fill:#0d1b2a,stroke:#00d2ff,color:#fff style CF fill:#1a0d2e,stroke:#f48120,color:#fff style DATA fill:#0d2e1a,stroke:#4ade80,color:#fff style EXTERNAL fill:#2e0d0d,stroke:#ff6b6b,color:#fff

Stack Completa Cada tecnologia e por que

Camada Tecnologia Funcao Motivo
Mobile React Native + Expo App iOS/Android TypeScript compartilhado com backend, expo-location excelente
Mobile @rnmapbox/maps Mapa interativo Melhor lib para poligonos customizados e performance
Mobile Zustand + React Query State management Zustand = leve/local, RQ = server state + offline
Mobile @turf/turf Geometria no device Deteccao de loop, area, interseccao — feedback instantaneo
API Cloudflare Workers Runtime Edge global, $5/mes, Hono roda nativamente nele
API Hono + tRPC Framework + API tipada 14kb, nativo em Workers, type safety end-to-end
API Drizzle ORM Database ORM Type-safe, suporta PostGIS, SQL-like
API Durable Objects WebSocket rooms Rooms por geohash, billing 20:1, hibernacao automatica
API Cloudflare Queues Background jobs Decay, rankings, region merge — incluso, substitui BullMQ
API Hyperdrive DB connection pooling Pool persistente ao Neon, gratis, elimina cold connections
DB PostgreSQL + PostGIS Banco principal INEGOCIAVEL: ST_Contains, ST_Intersects, ST_Area, indices GiST
DB Neon Hosting Postgres Serverless, branching, auto-scaling
DB Upstash Redis Cache + Pub/Sub Territorios visiveis, WebSocket rooms, rate limiting
Auth Clerk Autenticacao Google/Apple login, bom SDK pra RN
Infra Cloudflare (Workers+DO+Queues+R2) Tudo-em-um API + WebSocket + Jobs + Storage, edge global, $5/mes
Infra Expo EAS Build mobile Build nativo na cloud, OTA updates
Infra Turborepo + pnpm Monorepo Tipos compartilhados mobile/API

Sistema de Territorios Regioes atomicas, camadas cumulativas, decomposicao geometrica

Conceito central: O mapa e composto de regioes atomicas que NUNCA se sobrepoem. Cada circuito adiciona +1 camada a tudo que cobre. Regioes existentes sao decompostas automaticamente.

Como camadas se acumulam

graph TD
    subgraph STEP1["Passo 1: Andrei circula 1000m2"]
        R1["Regiao R1
1000m2
1 camada
Dono: Andrei"] end STEP1 --> STEP2 subgraph STEP2["Passo 2: Andrei circula 500m2 DENTRO"] R1A["R1a: anel externo
500m2
1 camada"] R1B["R1b: centro
500m2
2 camadas
(1 outer + 1 inner)"] end STEP2 --> STEP3 subgraph STEP3["Passo 3: Andrei circula 100m2 no nucleo (3x)"] R1A2["Anel externo
500m2
1 camada"] R1B2["Anel medio
400m2
2 camadas"] R1C["Nucleo
100m2
5 camadas
(1+1+3)"] end style R1 fill:#1a2e1a,stroke:#4ade80,color:#fff style R1A fill:#1a2e1a,stroke:#4ade80,color:#fff style R1B fill:#0d3e0d,stroke:#4ade80,color:#fff style R1A2 fill:#1a2e1a,stroke:#4ade80,color:#fff style R1B2 fill:#0d3e0d,stroke:#4ade80,color:#fff style R1C fill:#004400,stroke:#4ade80,color:#fff

Ataque Subtrativo: como disputas funcionam

Cada camada de ataque remove 1 camada do defensor. Se chegar a 0, o atacante toma. Isso significa que ataques parciais tem valor — voce enfraquece o oponente mesmo sem tomar.

graph LR
    subgraph ANTES["ANTES: Andrei domina"]
        A_EXT["Anel externo
1 camada"] A_MED["Anel medio
3 camadas"] A_NUC["Nucleo
6 camadas"] end ATAQUE["Maria circula
TUDO 2x"] subgraph DEPOIS["DEPOIS: Maria toma o anel, Andrei mantem o resto"] D_EXT["Anel externo
MARIA: 1 cam
(2 > 1, tomou)"] D_MED["Anel medio
ANDREI: 1 cam
(3 - 2 = 1)"] D_NUC["Nucleo
ANDREI: 4 cam
(6 - 2 = 4)"] end ANTES --> ATAQUE --> DEPOIS style A_EXT fill:#1a2e1a,stroke:#4ade80,color:#fff style A_MED fill:#0d3e0d,stroke:#4ade80,color:#fff style A_NUC fill:#004400,stroke:#4ade80,color:#fff style D_EXT fill:#3e1a2e,stroke:#ff6b6b,color:#fff style D_MED fill:#1a2e1a,stroke:#4ade80,color:#fff style D_NUC fill:#0d3e0d,stroke:#4ade80,color:#fff style ATAQUE fill:#3e1a1a,stroke:#ff6b6b,color:#fff

Resultado: Territorios-Ilha!

Andrei agora tem "ilhas" — territorios DENTRO do territorio da Maria. Ela controla o anel externo (as "estradas"), ele mantem fortalezas internas. Para Andrei recuperar, precisa correr e cercar a area da Maria.

Cenario complexo: Circuito cruzando sub-areas

graph TD
    subgraph ANTES2["ANTES: Andrei tem area grande + 2 sub-areas"]
        direction LR
        BA["Area grande: 1 cam"]
        BA --> SA["Sub A: +3 cam
(total 4)"] BA --> SB["Sub B: +2 cam
(total 3)"] end NOVO["Andrei faz novo circuito
cobrindo metade de A + metade de B + espaco entre"] subgraph DEPOIS2["DEPOIS: 6 regioes atomicas"] direction LR D_GRANDE["Grande
(fora de tudo)
1 cam"] D_A1["A nao coberto
4 cam"] D_A2["A coberto
5 cam (+1)"] D_ENTRE["Entre A e B
2 cam (+1)"] D_B2["B coberto
4 cam (+1)"] D_B1["B nao coberto
3 cam"] end ANTES2 --> NOVO --> DEPOIS2 style BA fill:#1a2e1a,stroke:#4ade80,color:#fff style SA fill:#004400,stroke:#4ade80,color:#fff style SB fill:#0d3e0d,stroke:#4ade80,color:#fff style D_GRANDE fill:#1a2e1a,stroke:#4ade80,color:#fff style D_A1 fill:#004400,stroke:#4ade80,color:#fff style D_A2 fill:#003300,stroke:#fff,color:#fff style D_ENTRE fill:#0d3e0d,stroke:#4ade80,color:#fff style D_B2 fill:#004400,stroke:#4ade80,color:#fff style D_B1 fill:#0d3e0d,stroke:#4ade80,color:#fff

Cenario: 3 jogadores na mesma area

graph TD
    subgraph MAPA["Estado do mapa"]
        direction TB
        ANDREI_AREA["ANDREI
Area grande: 2 camadas"] MARIA_AREA["MARIA
Tomou pedaco: 3 camadas"] CARLOS_AREA["CARLOS
Tomou pedaco: 1 camada"] end JOAO["JOAO circula TUDO 1x"] subgraph RESULTADO["Resultado (ataque subtrativo)"] direction TB R_AND["ANDREI: 2-1 = 1 cam
(enfraquecido)"] R_MAR["MARIA: 3-1 = 2 cam
(enfraquecida)"] R_CAR["JOAO toma!
Carlos 1-1 = 0
Joao: 1 cam"] R_LIVRE["Areas vazias:
JOAO 1 cam"] end MAPA --> JOAO --> RESULTADO style ANDREI_AREA fill:#1a1a3e,stroke:#00d2ff,color:#fff style MARIA_AREA fill:#3e1a2e,stroke:#ff6b6b,color:#fff style CARLOS_AREA fill:#1a3e1a,stroke:#4ade80,color:#fff style JOAO fill:#3e3a1a,stroke:#facc15,color:#fff style R_AND fill:#1a1a3e,stroke:#00d2ff,color:#fff style R_MAR fill:#3e1a2e,stroke:#ff6b6b,color:#fff style R_CAR fill:#3e3a1a,stroke:#facc15,color:#fff style R_LIVRE fill:#3e3a1a,stroke:#facc15,color:#fff

Algoritmo de processamento

flowchart TD
    START(["Novo claim: poligono P do jogador X"]) --> FIND["PostGIS: buscar regioes
que intersectam P"] FIND --> LOOP{"Para cada
regiao R
encontrada"} LOOP --> INTER["Calcular intersecao:
ST_Intersection(R, P)"] LOOP --> REMAIN["Calcular resto:
ST_Difference(R, P)"] REMAIN --> KEEP["Manter regiao do resto
(mesmas camadas, mesmo dono)"] INTER --> OWNER{"R.dono == X?
(mesmo jogador)"} OWNER -->|Sim| ADD["Reforco!
Camadas + 1"] OWNER -->|Nao| SUBTRACT["Ataque!
Camadas - 1"] SUBTRACT --> ZERO{"Camadas
== 0?"} ZERO -->|Sim| TAKE["X toma a regiao!
Camadas = 1"] ZERO -->|Nao| WEAKEN["Regiao enfraquecida
Mesmo dono, menos camadas"] LOOP --> FREE["Calcular area livre:
ST_Difference(P, todas as regioes)"] FREE --> NEW_REGION["Criar regiao nova
Dono: X, Camadas: 1"] ADD --> MERGE TAKE --> MERGE WEAKEN --> MERGE KEEP --> MERGE NEW_REGION --> MERGE MERGE["Merge: unir regioes adjacentes
com mesmo dono + mesmas camadas"] MERGE --> DONE(["Broadcast via WebSocket"]) style START fill:#1a1a2e,stroke:#00d2ff,color:#fff style TAKE fill:#3e1a3e,stroke:#ff6b6b,color:#fff style ADD fill:#1a3e1a,stroke:#4ade80,color:#fff style DONE fill:#1a1a2e,stroke:#7b2ff7,color:#fff

Estrategias que emergem

Estrategias ofensivas

  • Guerrilha — Muitos circuitos pequenos dentro do territorio inimigo. Enfraquece sem tomar. Prepara ataque grande.
  • Cerco — Cercar o territorio por fora, isolando em ilha. Oponente mantem camadas mas fica encurralado.
  • Blitz — Circuito gigante unico sobre area fraca. Toma tudo de uma vez.
  • Desgaste — Correr sobre a mesma area do oponente repetidamente. Desgasta camada por camada.

Estrategias defensivas

  • Fortaleza — Area pequena com muitas camadas. Dificil de tomar.
  • Defesa em profundidade — Area grande com nucleo fortificado. Atacante gasta energia no anel externo.
  • Reconquista — Correr sobre seu proprio territorio para restaurar camadas perdidas.
  • Expansao rapida — Areas grandes com 1 camada. Facil de perder, mas domina muito espaco no ranking.

Decisao de design: Ataque Subtrativo

Cada camada de ataque remove 1 camada do defensor. Se chegar a 0, atacante toma.

  • Ataques parciais tem valor — encoraja acao
  • Guerras de atrito sao divertidas
  • Fortalezas "impenetraveis" requerem esforco real
  • Ninguem fica invencivel — sempre da pra desgastar

Modelo de Dados Entidades e relacionamentos — modelo de regioes atomicas

erDiagram
    players ||--o{ runs : "faz"
    players ||--o{ regions : "possui"
    players ||--o{ claims : "reivindica"
    players ||--o{ region_reports : "reporta"
    runs ||--o{ trail_points : "contem"
    runs ||--o{ claims : "gera"
    regions ||--o{ region_history : "tem historico"
    regions ||--o{ region_reports : "recebe report"

    players {
        uuid id PK
        string clerk_id UK
        string username UK
        string display_name
        string avatar_url
        string color
        float total_area_m2
        int total_runs
        timestamp last_active
        timestamp created_at
    }

    runs {
        uuid id PK
        uuid player_id FK
        timestamp started_at
        timestamp ended_at
        float distance_m
        int duration_s
        float avg_pace
        string status
        jsonb device_info
        int confidence_score
    }

    trail_points {
        uuid id PK
        uuid run_id FK
        float lat_lng_alt
        float accuracy_speed
        timestamp timestamp
        float heading
        float accel_xyz
        float gyro_xyz
    }

    regions {
        uuid id PK
        geometry geom_POLYGON
        uuid owner_id FK
        int layer_count
        string geohash
        float area_m2
        timestamp created_at
        timestamp updated_at
    }

    claims {
        uuid id PK
        uuid player_id FK
        uuid run_id FK
        geometry original_geom
        float area_m2
        int confidence_score
        string status
        int regions_affected
        int regions_taken
        timestamp created_at
    }

    region_history {
        uuid id PK
        uuid region_id FK
        uuid player_id FK
        string action
        int layer_before
        int layer_after
        uuid owner_before FK
        uuid owner_after FK
        timestamp timestamp
    }

    region_reports {
        uuid id PK
        uuid region_id FK
        uuid reporter_id FK
        string reason
        text description
        string status
        timestamp created_at
    }

    admin_settings {
        string key PK
        jsonb value
        timestamp updated_at
        string updated_by
    }
            

Flow: Territory Claim Passo a passo de quando voce fecha um loop

1

Jogador corre e trail fecha loop

O device detecta que o trail cruzou a si mesmo. Calcula o poligono e a area localmente usando Turf.js. Se area >= 100m2, prepara o claim.

2

Envia claim para o servidor

Payload: poligono (GeoJSON), trail completo com timestamps, dados de sensor (acelerometro, giroscopio), device attestation token.

3

Confidence Score calculado

Server analisa 6 sinais e gera score 0-100. Abaixo de 20 = rejeitado. 20-49 = pendente. 50+ = continua processamento.

4

PostGIS verifica intersecoes

ST_Intersects busca territorios existentes que se sobrepoem ao novo poligono. Indice GiST garante query < 100ms mesmo com milhoes de registros.

5

Logica de dominio

Area nova? Cria territorio. Overlap com territorio proprio? Incrementa camadas. Overlap com territorio alheio? Compara camadas — se voce tem mais, toma o territorio.

6

Broadcast real-time

WebSocket envia update para todos os jogadores na mesma geohash cell. Mapa de todos atualiza em < 2 segundos.

flowchart TD
    START(["Jogador fecha loop"]) --> AREA{"Area >= 100m2?"}
    AREA -->|Nao| IGNORE["Ignorado
(muito pequeno)"] AREA -->|Sim| SEND["Envia claim
poligono + sensores"] SEND --> SCORE["Calcula Confidence Score"] SCORE --> CHECK{"Score?"} CHECK -->|"0-19"| REJECT["REJEITADO
Muito suspeito"] CHECK -->|"20-49"| PENDING["PENDENTE
Review manual"] CHECK -->|"50-100"| POSTGIS["PostGIS:
ST_Intersects"] POSTGIS --> OVERLAP{"Existe overlap?"} OVERLAP -->|Nao| NEW["Cria territorio novo
layer_count = 1"] OVERLAP -->|Sim| OWNER{"Mesmo dono?"} OWNER -->|Sim| STACK["Incrementa camadas
layer_count += 1"] OWNER -->|Nao| COMPARE{"Camadas do
novo > atual?"} COMPARE -->|Nao| REGISTER["Registra tentativa
(nao domina)"] COMPARE -->|Sim| TAKEOVER["TAKEOVER!
Novo dono"] NEW --> BROADCAST["WebSocket Broadcast"] STACK --> BROADCAST TAKEOVER --> BROADCAST REGISTER --> BROADCAST style START fill:#1a1a2e,stroke:#00d2ff,color:#fff style REJECT fill:#3e1a1a,stroke:#ff6b6b,color:#fff style PENDING fill:#3e3a1a,stroke:#facc15,color:#fff style NEW fill:#1a3e1a,stroke:#4ade80,color:#fff style STACK fill:#1a3e1a,stroke:#4ade80,color:#fff style TAKEOVER fill:#3e1a3e,stroke:#ff6b6b,color:#fff style BROADCAST fill:#1a1a3e,stroke:#7b2ff7,color:#fff

Sistema Anti-Cheat 6 camadas de protecao — sem limites artificiais

Filosofia: Se voce fez de verdade, e seu. A protecao e contra fraude, nao contra dedicacao.

graph TD
    CLAIM["Claim recebido"] --> L1

    subgraph L1["CAMADA 1: Device Attestation"]
        DA["Play Integrity / App Attest"]
        DA --> DA_CHECK{"Device
legitimo?"} DA_CHECK -->|Nao| DA_REJECT["REJEITADO
Device comprometido"] end DA_CHECK -->|Sim| L2 subgraph L2["CAMADA 2: Sensor Fusion"] SF["Acelerometro + Giroscopio vs GPS"] SF --> SF_SCORE["Gera sub-score
(0-25 pontos)"] end SF_SCORE --> L3 subgraph L3["CAMADA 3: Velocidade"] VEL["Limite: 45 km/h
(recorde Bolt: 44.72)"] VEL --> VEL_CHECK{">10% dos pontos
acima do limite?"} VEL_CHECK -->|Sim| VEL_FLAG["Marcado suspeito"] VEL_CHECK -->|Nao| VEL_OK["OK"] end VEL_FLAG --> L4 VEL_OK --> L4 subgraph L4["CAMADA 4: Confidence Score"] CS["Score = Device(25) + Sensor(25)
+ Cadencia(15) + Path(15)
+ Densidade(10) + Historico(10)"] CS --> CS_CHECK{"Score total?"} CS_CHECK -->|"80-100"| AUTO["Auto-aprovado"] CS_CHECK -->|"50-79"| MONITOR["Aprovado + monitorado"] CS_CHECK -->|"20-49"| REVIEW["Pendente review"] CS_CHECK -->|"0-19"| CS_REJECT["REJEITADO"] end AUTO --> L5 MONITOR --> L5 subgraph L5["CAMADA 5: Padroes Anomalos"] PA["Analise de comportamento
ao longo do tempo"] end L5 --> L6 subgraph L6["CAMADA 6: Community Reporting"] CR["Jogadores reportam
territorios suspeitos"] CR --> ADMIN["Painel Admin
review manual"] end style L1 fill:#1a0d0d,stroke:#ff6b6b,color:#fff style L2 fill:#0d1a2e,stroke:#00d2ff,color:#fff style L3 fill:#1a1a0d,stroke:#facc15,color:#fff style L4 fill:#0d1a0d,stroke:#4ade80,color:#fff style L5 fill:#1a0d2e,stroke:#a78bfa,color:#fff style L6 fill:#1a1a2e,stroke:#7b2ff7,color:#fff

Confidence Score — Composicao

Device Attest (25)
Sensor Fusion (25)
Cadencia (15)
Path (15)
Pts (10)
Hist (10)

O que detecta fraude

  • Celular parado na mesa + GPS "andando"
  • Velocidade constante perfeita (humanos oscilam)
  • Sem cadencia de passos no acelerometro
  • Path geometricamente perfeito (curvas perfeitas)
  • Teleportacao entre pontos
  • App modificado / device com root

O que NAO bloqueia

  • Areas gigantes (se correu de verdade)
  • Muitos km por dia (sem limite de stamina)
  • Velocidade alta (ate 45 km/h e OK)
  • Areas pequenas (min 100m2 = 10x10m)
  • Muitos claims por dia
  • Qualquer horario do dia

Sistema Real-Time WebSocket com Geohash Rooms

Para nao transmitir posicao de TODOS os jogadores para TODOS, o mapa e dividido em cells por geohash. Voce so recebe updates de jogadores proximos.

graph TB
    subgraph GRID["Mapa dividido em Geohash Cells (~1.2km x 0.6km)"]
        direction TB
        subgraph ROW1[" "]
            direction LR
            C1["6gkzwg"] ~~~ C2["6gkzwu
Jogador A"] ~~~ C3["6gkzwv"] end subgraph ROW2[" "] direction LR C4["6gkzwf
Jogador B"] ~~~ C5["6gkzws"] ~~~ C6["6gkzwt
Jogador C"] end subgraph ROW3[" "] direction LR C7["6gkzwd"] ~~~ C8["6gkzwe"] ~~~ C9["6gkzwh"] end end subgraph SERVER["WebSocket Server"] ROOM1["Room: 6gkzwu
subscribers: [A]"] ROOM2["Room: 6gkzwf
subscribers: [B]"] ROOM3["Room: 6gkzwt
subscribers: [C]"] REDIS_PS["Redis Pub/Sub
broadcast entre rooms vizinhas"] end C2 --> ROOM1 C4 --> ROOM2 C6 --> ROOM3 ROOM1 <--> REDIS_PS ROOM2 <--> REDIS_PS ROOM3 <--> REDIS_PS style GRID fill:#0d1b2a,stroke:#00d2ff,color:#fff style SERVER fill:#1a0d2e,stroke:#7b2ff7,color:#fff style C2 fill:#1a3e1a,stroke:#4ade80,color:#fff style C4 fill:#3e1a1a,stroke:#ff6b6b,color:#fff style C6 fill:#3e3a1a,stroke:#facc15,color:#fff

Como funciona

  • Jogador A esta no cell 6gkzwu. Se inscreve em 6gkzwu + 8 vizinhos (9 cells total)
  • Jogador A ve: B (cell vizinha) e C (cell vizinha)
  • Jogador D a 10km de distancia: A nao recebe nada dele
  • Quando A faz pan/zoom no mapa, o client recalcula cells e atualiza subscriptions
  • Updates de territorio: broadcast apenas para cells que contem/tocam o poligono

Sistema de Decay Territorios so decaem por abandono do app

timeline
    title Ciclo de Decay de um Territorio
    section Jogador ativo
        Dia 1-20 : Jogador usa o app normalmente
                  : Timer de decay resetado a cada sessao
                  : Territorios 100% seguros
    section Inatividade
        Dia 21 : Jogador para de abrir o app
        Dia 40 : Notificacao "Faz 20 dias!"
        Dia 45 : Notificacao "5 dias para decay!"
        Dia 49 : Notificacao "ULTIMO DIA!"
    section Decay ativo
        Dia 50 : -1 camada em todos os territorios
        Dia 57 : -1 camada (semanal)
        Dia 64 : -1 camada
        Dia 71 : Territorios com 0 camadas sao removidos
            

O que RESETA o timer

  • Abrir o app e ver o mapa (30s minimo)
  • Fazer qualquer corrida (em qualquer lugar)
  • Interagir com territorios (ver detalhes, ranking)
  • Qualquer sessao ativa de 30+ segundos

O que NAO precisa fazer

  • Nao precisa revisitar cada area
  • Nao precisa correr na area especifica
  • Nao precisa pagar nada
  • Pode ter 500 territorios — todos seguros se usar o app

Infraestrutura e Deploy Como tudo roda em producao

graph LR
    subgraph DEV["Desenvolvimento"]
        CODE["Codigo
Turborepo + pnpm"] --> GH["GitHub"] GH --> CI["GitHub Actions
lint, test, typecheck"] end subgraph BUILD["Build"] CI --> EAS["Expo EAS
Build iOS/Android"] CI --> WRANGLER["wrangler deploy
Cloudflare Workers"] end subgraph PROD["Producao"] EAS --> STORES["App Store
Google Play"] WRANGLER --> CF_PROD["Cloudflare
Workers + DO + Queues
(edge global, 200+ cidades)"] CF_PROD --> HYPER_P["Hyperdrive"] HYPER_P --> NEON["Neon PostgreSQL
+ PostGIS"] CF_PROD --> UPSTASH["Upstash Redis
(cache only)"] CF_PROD --> CF_R2["Cloudflare R2
Storage"] end subgraph MONITOR["Monitoramento"] API_PROD --> SENTRY_M["Sentry
Errors"] API_PROD --> PH["PostHog
Analytics"] end style DEV fill:#1a1a2e,stroke:#00d2ff,color:#fff style BUILD fill:#1a0d2e,stroke:#7b2ff7,color:#fff style PROD fill:#0d2e1a,stroke:#4ade80,color:#fff style MONITOR fill:#2e2a1a,stroke:#facc15,color:#fff

Custo Estimado

ServicoDev ($0)10k DAU50k DAU100k DAU
Cloudflare (Workers+DO+Queues)$0~$5~$15~$35
Neon (Postgres+PostGIS)$0~$15~$50~$120
Upstash Redis (cache)$0~$5~$15~$30
MapBox$0$0~$250~$500
Clerk (Auth)$0$0~$100~$200
Cloudflare R2$0~$5~$10~$20
Sentry + PostHog$0$0~$50~$100
TOTAL$0~$30/mes~$490/mes~$1.005/mes
vs stack anterior--66%-20%-25%

Estrutura do Monorepo

graph TD
    ROOT["turfy/"] --> APPS["apps/"]
    ROOT --> PKGS["packages/"]
    ROOT --> TURBO["turbo.json"]
    ROOT --> PNPM["pnpm-workspace.yaml"]

    APPS --> MOBILE["mobile/
React Native + Expo"] APPS --> API["api/
Hono + tRPC"] PKGS --> SHARED["shared/
types/ + validation/"] PKGS --> GEO["geo/
loop-detection.ts
polygon.ts
anti-cheat.ts"] PKGS --> DB["db/
Drizzle schema
migrations/"] MOBILE --> |"importa"| SHARED MOBILE --> |"importa"| GEO API --> |"importa"| SHARED API --> |"importa"| GEO API --> |"importa"| DB style ROOT fill:#1a1a2e,stroke:#00d2ff,color:#fff style MOBILE fill:#0d1b2a,stroke:#00d2ff,color:#fff style API fill:#1a0d2e,stroke:#7b2ff7,color:#fff style SHARED fill:#0d2e1a,stroke:#4ade80,color:#fff style GEO fill:#0d2e1a,stroke:#4ade80,color:#fff style DB fill:#0d2e1a,stroke:#4ade80,color:#fff