Documentação Técnica · Sistema Híbrido

Arquitetura Google Maps + Firebase
e Segurança para o Federal Pro

Guia completo de integração das APIs do Google Maps com o Firebase como orquestrador de estado, com segurança end-to-end: JWT, RBAC, proteção de PII (CPF, celular, localização), Firestore Rules, TLS pinning e rate limiting.

4APIs Essenciais
10KGrátis/mês
JWTAuth Segura
LGPDCompliance
01

Visão Geral

O que vamos construir e porquê

O objetivo é ter um sistema de ride-hailing com matching inteligente, rotas com trânsito em tempo real e experiência fluida — com segurança de nível bancário para proteger motoristas e passageiros.

Princípio central: o cliente React Native nunca chama o Google diretamente, nunca guarda a chave do Google e nunca confia em si mesmo. Todas as operações sensíveis passam por Firebase (Cloud Functions + Firestore + Auth) com JWT verificado.
02

APIs Necessárias

Excluindo o Maps SDK (usamos Leaflet)
API Para quê Prioridade Custo Dados sensíveis?
GeocodingCoordenadas → endereçoEssencial10K/mêsLocalização
Places AutocompleteSugerir endereçosEssencial10K/mêsQuery do user
Places DetailsCoordenadas do localEssencial10K/mêsPúblico
DirectionsRota + ETAEssencial10K/mêsTrajeto
Distance MatrixETAs em loteOpcional$5/1KLocalizações
RoadsSnap à malha viáriaOpcional$10/1KHistórico GPS
03

Funções Detalhadas

O que cada API faz, devolve e como usar
location_onGeocoding API

Converte coordenadas GPS em endereço legível (reverse geocoding) ou endereço em coordenadas.

Chamada segura

Sempre via Cloud Function. O resultado é cacheado por 30 dias (permitido pelos TOS).

searchPlaces Autocomplete

Devolve sugestões enquanto digita. Nunca chama diretamente do cliente — a query pode conter dados sensíveis.

Chamada segura

Proxy na Cloud Function + session token para agrupar billing.

placePlaces Details

Dado um place_id, devolve coordenadas e endereço formatado.

Chamada segura

Cache permanente no Firestore (place_id é imutável).

directions_carDirections API

Calcula rota + distância + ETA + trânsito em tempo real.

Chamada segura

Apenas para o motorista designado. Cached 5 min para o mesmo par origem-destino.

grid_onDistance Matrix API

Calcula ETA de múltiplos motoristas de uma vez.

Chamada segura

Só no backend, após filtragem por geohash. Nunca expõe lista completa de motoristas ao cliente.

add_roadRoads API

"Snap" de coordenadas GPS à malha viária real.

Chamada segura

Só após viagem concluída. Dados guardados encriptados no Firestore.

04

Firebase como Orquestrador

O cérebro por trás das APIs do Google
storageFirestore

Fonte única de verdade. Armazena localizações em tempo real, estado das viagens e resultados cacheados.

boltCloud Functions

Backend serverless. Executa chamadas às APIs do Google, lógica de negócio e verificação de JWT.

lockFirebase Auth

Gera o JWT (ID Token) após login. Cada chamada a Cloud Function verifica este token.

notificationsCloud Messaging

Notificações push com payload assinado. Nunca inclui dados sensíveis.

tuneRemote Config

Feature flags e configurações dinâmicas. Permite desativar funcionalidades em caso de incidente.

analyticsAnalytics

Métricas anonimizadas. Nunca envia PII (CPF, celular, localização precisa).

05

Arquitetura Híbrida

Como cada peça se liga às outras
src/ ├── services/ │ ├── googleMaps/ │ │ ├── client.ts ← wrapper base (via proxy) │ │ ├── geocoding.ts ← coords ↔ endereço │ │ ├── places.ts ← autocomplete + details │ │ ├── directions.ts ← rotas + trânsito │ │ └── distanceMatrix.ts ← ETAs em lote │ ├── firebase.ts ← listeners + auth │ └── security/ ← NOVO │ ├── jwt.ts ← gestão de tokens │ ├── crypto.ts ← encriptação local │ └── biometric.ts ← biometria para operações críticas ├── hooks/ │ ├── useAuth.ts │ ├── useRoute.ts │ ├── useAddressSearch.ts │ └── useSecureStorage.ts ← NOVO └── app/(app)/ └── index.tsx functions/ ├── auth.ts ← NOVO: login, signup, refresh ├── matchDriver.ts ├── calculateRoute.ts ├── reverseGeocode.ts └── middleware.ts ← NOVO: verifyJWT, rateLimit
06

Fluxo Completo

Do login ao motorista, passo a passo
  • Login/Registo. O utilizador envia email+password ou telefone+SMS. Firebase Auth gera ID Token (JWT) + Refresh Token.
  • Guardar tokens. ID Token (1h) em memória, Refresh Token em expo-secure-store (Keychain/Keystore).
  • Cada chamada à API. O cliente envia Authorization: Bearer <ID Token>.
  • Cloud Function verifica o JWT. Assinatura, expiração, aud, iss, custom claims.
  • Verificação RBAC. O role no token define o que pode fazer (driver/passenger/admin).
  • Ação é executada. Ex: matching, cálculo de rota, escrita no Firestore.
  • Resposta assinada. O cliente recebe dados filtrados apenas para aquele utilizador.
  • Refresh automático. Se o ID Token expirar, o SDK usa o Refresh Token para obter um novo.
  • Operações críticas. Pagamento, alteração de dados bancários exigem biometria local.
  • Log e auditoria. Cada acesso a dados sensíveis gera um registo auditável.
07

🔒 Segurança de Dados do Utilizador

Motoristas, passageiros e PII
PII (Personally Identifiable Information) — qualquer dado que identifica ou pode identificar uma pessoa. No Federal Pro: nome, CPF, celular, email, localização precisa, foto, dados bancários. Estes dados exigem proteção especial sob a LGPD.

📊 Classificação de Dados

Dado Classificação Onde guardar Encriptação Retenção
Nome completo PII Firestore (users/{uid}) TLS + at-rest (Google) Enquanto ativo + 5 anos
CPF PII Sensível Firestore encriptado AES-256 Encriptação aplicacional Enquanto ativo + 5 anos
Celular PII Firestore + Auth TLS + at-rest Enquanto ativo
Email PII Firebase Auth Hash + salt Enquanto ativo
Localização atual PII Sensível Firestore (TTL 90 dias) TLS 90 dias
Histórico de viagens Interno Firestore (trips/{id}) TLS + at-rest 2 anos
Dados bancários PCI-DSS Nunca no Firestore Tokenizado (Stripe/Mercado Pago) Nunca armazenar
Foto de perfil PII Cloud Storage (path privado) Signed URLs (TTL 15min) Enquanto ativo

🛡️ Estratégias de Proteção

visibility_offMinimização de dados

Recolhe apenas o estritamente necessário. Não peças CPF para agendar uma viagem. Não guardes localização exata depois de 90 dias.

Reduz superfície de ataque em ~60%

lockEncriptação AES-256 no CPF

O CPF é encriptado antes de chegar ao Firestore, com chave gerida no Google Secret Manager. Nunca é guardado em plaintext.

badgeMascarar PII na UI

Mostra apenas os últimos 2 dígitos do CPF, últimos 4 do celular. O utilizador confirma identidade, não lê o dado completo.

•••.•••.•••-42  |  (11) 9••••-1234

securityTokenização de pagamentos

Nunca guardar número de cartão. Usar tokens do Stripe/Mercado Pago/Pagar.me. PCI-DSS é responsabilidade do gateway.

timerTTL automático

Localização atual expira em 90 dias. Sessões em 30 dias. Logs em 1 ano. Firestore TTL policies automatizam isto.

historyAuditoria de acessos

Cada leitura de PII (CPF, celular, localização) é registada em audit_logs com uid, action, timestamp e ip.

📝 Exemplo: Encriptar CPF antes de guardar

functions/security/crypto.ts
import * as crypto from 'crypto';
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';

const client = new SecretManagerServiceClient();
let cachedKey: Buffer | null = null;

async function getKey(): Promise<Buffer> {
  if (cachedKey) return cachedKey;
  const [version] = await client.accessSecretVersion({
    name: 'projects/federal-pro/secrets/pii-key/versions/latest',
  });
  cachedKey = Buffer.from(version.payload.data as Uint8Array);
  return cachedKey;
}

/** Encripta PII com AES-256-GCM (autenticado) */
export async function encryptPII(plain: string): Promise<string> {
  const key = await getKey();
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);

  const encrypted = Buffer.concat([
    cipher.update(plain, 'utf8'),
    cipher.final(),
  ]);
  const authTag = cipher.getAuthTag();

  // Formato: iv.authTag.encrypted (base64)
  return [iv, authTag, encrypted].map((b) => b.toString('base64')).join('.');
}

export async function decryptPII(payload: string): Promise<string> {
  const [ivB64, tagB64, dataB64] = payload.split('.');
  const key = await getKey();

  const decipher = crypto.createDecipheriv(
    'aes-256-gcm',
    key,
    Buffer.from(ivB64, 'base64')
  );
  decipher.setAuthTag(Buffer.from(tagB64, 'base64'));

  return Buffer.concat([
    decipher.update(Buffer.from(dataB64, 'base64')),
    decipher.final(),
  ]).toString('utf8');
}

🎭 Mascarar PII na UI (cliente)

src/utils/mask.ts
/** Mascara CPF mostrando apenas os últimos 2 dígitos */
export function maskCPF(cpf?: string): string {
  if (!cpf) return '—';
  const d = String(cpf).replace(/\D/g, '');
  if (d.length < 2) return '•••.•••.•••-••';
  return `•••.•••.•••-${d.slice(-2)}`;
}

/** Mascara celular mostrando DDD + últimos 4 */
export function maskPhone(phone?: string): string {
  if (!phone) return '—';
  const d = String(phone).replace(/\D/g, '');
  if (d.length < 11) return '(••) •••••-••••';
  return `(${d.slice(0, 2)}) ${d.slice(2, 3)}••••-${d.slice(-4)}`;
}

/** Mascara email */
export function maskEmail(email?: string): string {
  if (!email || !email.includes('@')) return '—';
  const [user, domain] = email.split('@');
  const visible = user.slice(0, 2);
  return `${visible}${'•'.repeat(Math.max(user.length - 2, 2))}@${domain}`;
}
08

🔐 JWT & Autenticação

Login, cadastro e verificação em cada pedido

O JWT (JSON Web Token) é o mecanismo central de autenticação. Cada pedido a uma Cloud Function ou ao Firestore carrega um token que prova a identidade do utilizador. O backend nunca confia no cliente — verifica sempre a assinatura e as permissões.

🎫 Anatomia do JWT

JWT structure
// Header
{ "alg": "RS256", "typ": "JWT", "kid": "firebase-key-1" }

// Payload (claims)
{
  "iss": "https://securetoken.google.com/federal-pro",
  "aud": "federal-pro",
  "auth_time": 1790000000,
  "user_id": "abc123uid",
  "sub": "abc123uid",
  "iat": 1790000000,
  "exp": 1790003600,          // 1 hora
  "email": "motorista@exemplo.com",
  "email_verified": true,
  "phone_number": "+5511999998888",
  "role": "driver",           // custom claim
  "categoria": "pro",          // custom claim
  "status": "aprovado"          // custom claim
}

// Signature (RS256) — verifica com chave pública do Firebase
Regras críticas: o payload do JWT é assinado, mas não encriptado. Qualquer pessoa pode ler o conteúdo (user_id, email). Nunca coloques CPF, dados bancários ou localização no payload. Para esses dados, usa o Firestore com Security Rules.

🔄 Ciclo de Vida dos Tokens

Token Duração Onde guardar Para quê
ID Token (JWT) 1 hora Memória (nunca em disco) Autorizar pedidos às APIs
Refresh Token 30 dias SecureStore (Keychain/Keystore) Obter novo ID Token quando expira
Session Token (Places) Até fechar sessão de autocomplete Memória Agrupar billing de autocomplete
Custom Claims Refrescados ao pedir novo ID Token Dentro do ID Token RBAC (role, categoria, status)

📝 Implementação: Login com JWT

src/services/security/jwt.ts
import * as SecureStore from 'expo-secure-store';
import auth from '@react-native-firebase/auth';

const REFRESH_KEY = 'fp_refresh_token';

// Guardar o refresh token em armazenamento seguro (Keychain/Keystore)
export async function persistRefreshToken(token: string) {
  await SecureStore.setItemAsync(REFRESH_KEY, token, {
    keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
  });
}

export async function getRefreshToken() {
  return SecureStore.getItemAsync(REFRESH_KEY);
}


export async function clearTokens() {
  await SecureStore.deleteItemAsync(REFRESH_KEY);
}

/** Devolve um ID Token válido (renova automaticamente se expirado) */
export async function getValidIdToken(forceRefresh = false): Promise<string | null> {
  const user = auth().currentUser;
  if (!user) return null;
  return user.getIdToken(forceRefresh);
}

/** Wrapper que injeta o JWT em todos os pedidos HTTP */
export async function secureFetch(url: string, options: RequestInit = {}) {
  const token = await getValidIdToken();

  const res = await fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
  });

  // Se 401, força refresh do token e tenta 1x
  if (res.status === 401) {
    const fresh = await getValidIdToken(true);
    return fetch(url, {
      ...options,
      headers: { ...options.headers, 'Authorization': `Bearer ${fresh}` },
    });
  }

  return res;
}

🛡️ Verificar JWT no Backend

functions/middleware.ts
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';

/** Wrapper que exige JWT válido em qualquer Cloud Function */
export function withAuth(
  handler: (data: any, ctx: AuthContext) => Promise<any>,
  requiredRole?: 'driver' | 'passenger' | 'admin'
) {
  return functions.https.onCall(async (data, context) => {
    // 1. Verifica se há token
    if (!context.auth) {
      throw new functions.https.HttpsError(
        'unauthenticated', 'JWT ausente ou inválido'
      );
    }

    const { uid, token } = context.auth;

    // 2. Verifica se o token não foi revogado
    const revoked = await admin.auth().verifyIdToken(context.auth.token.toString(), true);

    // 3. Verifica role (RBAC)
    if (requiredRole && token.role !== requiredRole) {
      throw new functions.https.HttpsError(
        'permission-denied',
        `Requer role '${requiredRole}', tem '${token.role}'`
      );
    }

    // 4. Verifica se a conta está aprovada
    if (token.status !== 'aprovado') {
      throw new functions.https.HttpsError(
        'permission-denied', 'Conta pendente de aprovação'
      );
    }

    // 5. Rate limiting (por uid)
    const allowed = await checkRateLimit(uid, 60, '1m');
    if (!allowed) {
      throw new functions.https.HttpsError(
        'resource-exhausted', 'Demasiados pedidos'
      );
    }

    // 6. Auditoria
    await admin.firestore().collection('audit_logs').add({
      uid, action: 'call', function: handler.name,
      timestamp: admin.firestore.FieldValue.serverTimestamp(),
    });

    return handler(data, { uid, role: token.role, claims: token });
  });
}

type AuthContext = {
  uid: string;
  role: 'driver' | 'passenger' | 'admin';
  claims: any;
};

🔑 Custom Claims — Definir Role

functions/auth/onUserCreate.ts
import * as admin from 'firebase-admin';
import * as functions from 'firebase-functions';

/** Quando um utilizador é criado, define custom claims */
export const onUserCreate = functions.auth.user().onCreate(
  async (user) => {
    // Todos começam como passageiro até aprovação
    await admin.auth().setCustomUserClaims(user.uid, {
      role: 'passenger',
      categoria: 'light',
      status: 'pendente',
    });

    // Cria documento no Firestore com dados públicos
    await admin.firestore().collection('users').doc(user.uid).set({
      email: user.email ?? null,
      phone: user.phoneNumber ?? null,
      role: 'passenger',
      status: 'pendente',
      createdAt: admin.firestore.FieldValue.serverTimestamp(),
    });
  }
);

/** Promover para motorista (chamado pelo admin) */
export const promoteToDriver = functions.https.onCall(
  async (data, context) => {
    if (context.auth?.token.role !== 'admin') {
      throw new functions.https.HttpsError('permission-denied', 'Só admin');
    }

    const { uid, categoria } = data;

    await admin.auth().setCustomUserClaims(uid, {
      role: 'driver',
      categoria,
      status: 'aprovado',
    });

    // Força refresh do token no cliente
    await admin.auth().revokeRefreshTokens(uid);

    return { ok: true };
  }
);
09

🌐 Segurança do Tráfego

HTTPS, TLS pinning, rate limiting, WAF
enhanced_encryptionHTTPS obrigatório (TLS 1.3)

Todo o tráfego cliente ↔ backend usa TLS 1.3. Cloud Functions e Firestore já o fazem por defeito. Proíbe HTTP no domínio.

verified_userCertificate Pinning

No app, fixa o certificado SSL do teu backend para impedir ataques man-in-the-middle com certificados falsos.

speedRate Limiting

Limite de pedidos por uid, IP e endpoint. Ex: 60/min para APIs normais, 5/min para operações sensíveis (login, pagamento).

shieldCORS rigoroso

Cloud Functions apenas aceitam pedidos dos domínios autorizados. Bloqueia * em produção.

fingerprintApp Check

Firebase App Check valida que os pedidos vêm do teu app genuíno (Play Integrity / DeviceCheck). Bloqueia bots e scripts.

radarDeteção de anomalias

Padrões suspeitos (login de IP diferente, muitas viagens em 1min) disparam 2FA ou bloqueio temporário.

travel_exploreWAF no backend

Cloud Armor no Google Cloud filtra SQL injection, XSS, DDoS. Regras pré-configuradas pela Google.

vpn_lockPayload assinado

Notificações push e webhooks críticos têm assinatura HMAC-SHA256. O cliente verifica antes de agir.

🔒 Certificate Pinning no App

app.json (Expo)
{
  "expo": {
    "plugins": [
      ["expo-build-properties", {
        "android": {
          "networkSecurityConfig": "./network_security_config.xml"
        },
        "ios": {
          "useFrameworks": "static"
        }
      }]
    ]
  }
}
network_security_config.xml (Android)
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <domain-config cleartextTrafficPermitted="false">
    <domain includeSubdomains="true">federalpro.com.br</domain>
    <domain includeSubdomains="true">cloudfunctions.net</domain>

    <pin-set expiration="2026-12-31">
      <!-- SHA-256 do certificado (extrair com openssl) -->
      <pin digest="SHA-256">AAAA...base64...=</pin>
      <!-- Backup pin (rotação) -->
      <pin digest="SHA-256">BBBB...base64...=</pin>
    </pin-set>
  </domain-config>

  <!-- Bloquear tudo o resto em cleartext -->
  <base-config cleartextTrafficPermitted="false">
    <trust-anchors>
      <certificates src="system" />
    </trust-anchors>
  </base-config>
</network-security-config>

⏱️ Rate Limiting no Backend

functions/security/rateLimit.ts
import * as admin from 'firebase-admin';

/** Sliding window rate limit usando Firestore + TTL */
export async function checkRateLimit(
  identifier: string,
  maxRequests: number,
  window: '1m' | '1h' | '1d' = '1m'
): Promise<boolean> {
  const windowMs = { '1m': 60_000, '1h': 3_600_000, '1d': 86_400_000 }[window];
  const now = Date.now();
  const bucket = Math.floor(now / windowMs);
  const docRef = admin.firestore()
    .collection('rate_limits')
    .doc(`${identifier}_${bucket}`);

  const allowed = await admin.firestore().runTransaction(async (tx) => {
    const doc = await tx.get(docRef);
    const count = (doc.data()?.count ?? 0) + 1;

    if (count > maxRequests) return false;

    tx.set(docRef, {
      count,
      expiresAt: admin.firestore.Timestamp.fromMillis(now + windowMs),
    }, { merge: true });

    return true;
  });

  return allowed;
}

// Configurar TTL no Firestore Console:
// Collection: rate_limits | Field: expiresAt
10

🛂 RBAC & Firestore Rules

Quem pode ler e escrever o quê

👥 Roles e Permissões

Role Pode ler Pode escrever Endpoints autorizados
passenger O seu próprio perfil, viagens onde é participante O seu perfil, criar pedidos de viagem createTripRequest, getRoute
driver O seu perfil, viagens atribuídas, dados mínimos do passageiro O seu perfil, localização, aceitar/rejeitar viagens matchDriver, acceptTrip, updatePosition
admin Tudo, dentro do âmbito operacional Promover utilizadores, alterar tarifas, bloquear contas promoteToDriver, blockUser, setTariffs

🔥 Firestore Security Rules

firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // ---------- Helpers ----------
    function isSignedIn() {
      return request.auth != null;
    }

    function isOwner(uid) {
      return isSignedIn() && request.auth.uid == uid;
    }

    function isRole(role) {
      return isSignedIn() && request.auth.token.role == role;
    }

    function isApproved() {
      return isSignedIn() && request.auth.token.status == 'aprovado';
    }

    function isParticipant() {
      return isSignedIn()
        && request.auth.uid in resource.data.participants;
    }

    // ---------- Users ----------
    match /users/{uid} {
      // Só o próprio utilizador lê o seu perfil completo
      allow read: if isOwner(uid) || isRole('admin');

      // Só o próprio pode atualizar o seu perfil, e não pode alterar role/status
      allow update: if isOwner(uid)
        && !request.resource.data.diff(resource.data).affectedKeys()
             .hasAny(['role', 'status', 'cpf_encrypted']);

      // Ninguém apaga o próprio perfil (só admin)
      allow delete: if isRole('admin');
    }

    // ---------- Drivers (localização em tempo real) ----------
    match /drivers/{driverId} {
      // Motorista escreve apenas no seu próprio documento
      allow write: if isOwner(driverId) && isRole('driver') && isApproved();

      // Qualquer utilizador autenticado pode ler (necessário para matching)
      // MAS apenas campos públicos (não inclui cpf, email, etc.)
      allow read: if isSignedIn();
    }

    // ---------- Trips ----------
    match /trips/{tripId} {
      allow read: if isParticipant() || isRole('admin');
      allow create: if isSignedIn()
        && request.resource.data.participants.hasAll([request.auth.uid]);
      allow update: if isParticipant();
    }

    // ---------- Match requests (efémeros) ----------
    match /match_requests/{uid} {
      allow read: if isOwner(uid);
      // Escrita só via Cloud Function (Admin SDK)
      allow write: if false;
    }

    // ---------- Cache (geocoding, places) ----------
    match /cache/{type}/{hash} {
      allow read: if isSignedIn();
      // Nunca escrever diretamente — só Cloud Functions
      allow write: if false;
    }

    // ---------- Audit logs ----------
    match /audit_logs/{logId} {
      allow read: if isRole('admin');
      allow write: if false; // apenas Admin SDK
    }

    // ---------- Tudo o resto: negar por defeito ----------
    match /{document=**} {
      allow read, write: if false;
    }
  }
}

🗄️ Storage Rules (fotos, documentos)

storage.rules
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {

    // Fotos de perfil — só o próprio lê/escreve
    match /avatars/{uid}/{fileName} {
      allow read: if request.auth.uid == uid;
      allow write: if request.auth.uid == uid
        && request.resource.size < 5 * 1024 * 1024 // 5MB
        && request.resource.contentType.matches('image/.*');
    }

    // Documentos (CNH, CRLV) — só o próprio e admins
    match /documents/{uid}/{docId} {
      allow read: if request.auth.uid == uid
        || request.auth.token.role == 'admin';
      allow write: if request.auth.uid == uid
        && request.resource.size < 10 * 1024 * 1024;
    }

    // Nada mais é acessível
    match /{allPaths=**} {
      allow read, write: if false;
    }
  }
}
11

Otimização de Custos

Reduzir a fatura sem perder qualidade
saveCache de Geocoding (TTL 30 dias)

Os TOS permitem cachear até 30 dias. Guarda no Firestore com hash das coordenadas arredondadas.

Poupança: ~70%

pin_dropArredondar coordenadas

Cacheia -22.90841 em vez de -22.9084125.

Poupança: ~50% das chaves

filter_altGeofire antes de Distance Matrix

Filtra por geohash primeiro. Envia só 10-20 candidatos.

Poupança: ~95%

hourglass_emptyDebounce no autocomplete

Só chama Places após 300ms sem digitar.

Poupança: ~80%

monitoringAlertas de orçamento

Define limite mensal no Cloud Console. Desativa APIs se ultrapassar.

blockDesativar APIs não usadas

Cada API ativa é superfície de ataque + linha na fatura.

12

Otimização de Desempenho

Latência baixa, app fluida
boltListeners vs polling

Firestore onSnapshot() em vez de polling. Latência ~100ms.

polylinePolyline lazy

Renderiza só pontos no viewport + simplifica com Douglas-Peucker.

cloudRegião Cloud Functions

southamerica-east1. Latência de 200ms → 30ms.

rocket_launchPreload paralelo

Promise.all() ao fazer login. Não esperes em série.

13

Otimização de Recursos

Bateria, memória, rede
battery_saverBateria
  • GPS accuracy: Balanced
  • Intervalo 5-10s
  • Pausar quando offline
memoryMemória
  • Unsubscribe listeners
  • FlatList > ScrollView
  • useMemo em cálculos
wifiRede
  • Offline-first com fila
  • gzip nas Functions
  • Batch coordenadas
14

Experiência do Utilizador

Rápido, claro, seguro
flash_onFeedback <100ms

Micro-scale, foco animado, skeleton em vez de spinner.

near_meLocalização otimista

Mostra posição aproximada imediatamente. Endereço depois.

fingerprintBiometria para crítico

FaceID/impressão digital para pagamentos, mudança de senha, apagar conta.

errorErros amigáveis

"Sem internet" em vez de "Network request failed".

vibrationHaptics

Vibração leve em ações importantes.

dark_modeModo escuro

Motorista trabalha à noite. Reduz fadiga e poupa bateria.

15

Código Prático

Cloud Function de matching com JWT + RBAC + Cache
functions/matchDriver.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
import { getGeohashRange } from 'geofire-common';
import axios from 'axios';
import { withAuth } from './middleware';

admin.initializeApp();
const db = admin.firestore();
const GOOGLE_KEY = functions.config().google.key;
const RAIO_KM = 5;

// withAuth exige JWT válido + role passenger + rate limit
export const matchDriver = withAuth(async (data, ctx) => {
  const { lat, lng } = data;

  // Validar input (defesa contra injection)
  if (typeof lat !== 'number' || typeof lng !== 'number') {
    throw new functions.https.HttpsError('invalid-argument', 'Coords inválidas');
  }

  const range = getGeohashRange(lat, lng, RAIO_KM * 1000);

  const snapshot = await db.collection('drivers')
    .where('status', '==', 'online')
    .where('geohash', '>=', range.lower)
    .where('geohash', '<=', range.upper)
    .limit(20)
    .get();

  if (snapshot.empty) {
    return { drivers: [], message: 'Nenhum motorista próximo' };
  }

  const drivers = snapshot.docs.map((d) => {
    const data = d.data();
    // Retorna apenas campos públicos — nunca cpf, email, etc.
    return {
      id: d.id,
      lat: data.lat,
      lng: data.lng,
      name: data.name,
      rating: data.rating,
      vehicle: data.vehicle,
      fcmToken: data.fcmToken,
    };
  });

  const origins = drivers.map((d) => `${d.lat},${d.lng}`).join('|');
  const destination = `${lat},${lng}`;

  const { data: matrix } = await axios.get(
    'https://maps.googleapis.com/maps/api/distancematrix/json',
    { params: { origins, destinations: destination,
        departure_time: 'now', traffic_model: 'best_guess', key: GOOGLE_KEY } }
  );

  const elements = matrix.rows[0].elements;
  const candidates = drivers
    .map((driver, i) => ({
      driver,
      distance: elements[i].distance.value,
      durationInTraffic: elements[i].duration_in_traffic?.value ?? elements[i].duration.value,
    }))
    .filter((c) => c.durationInTraffic > 0)
    .sort((a, b) => a.durationInTraffic - b.durationInTraffic);

  const best = candidates[0];

  // Grava match com TTL de 30s
  await db.collection('match_requests').doc(ctx.uid).set({
    driverId: best.driver.id,
    distanceMeters: best.distance,
    etaSeconds: best.durationInTraffic,
    status: 'pending',
    createdAt: admin.firestore.FieldValue.serverTimestamp(),
    expiresAt: new Date(Date.now() + 30000),
  });

  // Push ao motorista (sem PII no payload)
  await admin.messaging().send({
    token: best.driver.fcmToken,
    notification: {
      title: 'Nova viagem!',
      body: `${(best.distance / 1000).toFixed(1)} km • ${Math.ceil(best.durationInTraffic / 60)} min`,
    },
    data: { matchId: ctx.uid, type: 'new_trip' },
  });

  // Devolve ao cliente apenas o necessário
  return {
    matchId: ctx.uid,
    driver: {
      name: best.driver.name,
      rating: best.driver.rating,
      vehicle: best.driver.vehicle,
    },
    distance: best.distance,
    eta: best.durationInTraffic,
  };
}, 'passenger');
16

Resumo Geral

Arquitetura + Segurança numa tabela
Componente Responsabilidade Segurança Onde
Firebase AuthLogin + JWTID Token RS256Backend
expo-secure-storeGuardar refresh tokenKeychain/KeystoreCliente
FirestoreEstado + cacheRules + AES-256 para PIIBackend
Cloud FunctionsOrquestraçãowithAuth + RBACBackend
Geocoding APICoords → endereçoProxy + cacheCloud Function
Places APIAutocompleteSession tokenCloud Function
Directions APIRotasSó para participanteCloud Function
Distance MatrixETAs em lotePós-GeoFireCloud Function
App CheckAnti-botPlay IntegrityBackend
Certificate PinningAnti-MITMTLS pinningCliente
Rate LimitingAnti-abusoSliding windowCloud Function
Audit LogsRastreabilidadeImutáveisBackend
Estratégia vencedora: Firebase orquestra, Google fornece dados, Leaflet desenha, GeoFire filtra. O JWT verificado em cada pedido + RBAC + Firestore Rules restritivas garantem que cada utilizador só vê e faz o que lhe compete. PII encriptado, mascarado na UI e com TTL. LGPD compliance desde o dia 1.