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
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
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
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
| 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 |
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.
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
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
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.
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
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
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
Cada camada de ataque remove 1 camada do defensor. Se chegar a 0, atacante toma.
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
}
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.
Payload: poligono (GeoJSON), trail completo com timestamps, dados de sensor (acelerometro, giroscopio), device attestation token.
Server analisa 6 sinais e gera score 0-100. Abaixo de 20 = rejeitado. 20-49 = pendente. 50+ = continua processamento.
ST_Intersects busca territorios existentes que se sobrepoem ao novo poligono. Indice GiST garante query < 100ms mesmo com milhoes de registros.
Area nova? Cria territorio. Overlap com territorio proprio? Incrementa camadas. Overlap com territorio alheio? Compara camadas — se voce tem mais, toma o territorio.
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
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
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
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
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
| Servico | Dev ($0) | 10k DAU | 50k DAU | 100k 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% |
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