SOS Telha
Pro-bono

SOS Telha

Community platform connecting people who have roof tiles (supply) with people who need them (demand) after Storm Kristin in Leiria and Marinha Grande, Portugal.

Visit app
Next.jsSupabaseTailwindshadcn/uiLeafletVercel

February 16, 2025

SOS Telha: Support Network for Reconstruction

What It Is

SOS Telha is a "Rede de Apoio à Reconstrução" (support network for reconstruction) built pro-bono by KORVO in response to the devastation caused by Storm Kristin in the district of Leiria and Marinha Grande, Portugal.

Users publish either Oferta (I have tiles) or Procura (I need tiles), with optional mão de obra (I need labour to place tiles). Everything is shown on an interactive map so people can see what's nearby and avoid unnecessary trips.

Map and list view of SOS Telha (Desktop)Map and list view of SOS Telha (Desktop)

Mobile view of creating pedido

Mobile view of creating pedido — formMobile view of creating pedido — form

Part 2 — Uber-style location selectionPart 2 — Uber-style location selection

Who It's For

Residents and businesses in the Leiria/Marinha Grande area affected by Storm Kristin who have spare tiles to give or need tiles or labour to rebuild.

Features

  • Interactive map with clustering — see supply and demand nearby
  • List view with filters: tile type, logistics, "only my posts"
  • Sorting: newest first or closest to me
  • Safety banner warning that roof work is dangerous and should not be done alone
  • Admin area for moderating posts and feedback
  • Telegram and Discord alerts when users report suspicious posts — act immediately from the alert (e.g. delete the post) or jump to the admin panel

Why It Matters

After a calamity, matching supply and demand locally speeds up rebuilding and reduces waste. The map and filters make it easy to find or offer help nearby.

How We Built It

  • Next.js (App Router), Supabase (auth + data), Tailwind CSS, shadcn/ui
  • Leaflet for the map with clustering
  • React Hook Form + Zod for forms
  • Optional dark mode
  • Deployed on Vercel with analytics

Under the hood

Tile posts are loaded via a Next.js server action: "use server" runs the function on the server, we query Supabase for the latest rows, then map them to our TilePost type. One small file that shows the full flow from database to UI.

// app/actions/tile-posts.ts
"use server";

import { createClient } from "@/lib/supabase/server";
import { rowToTilePost } from "@/lib/tile-posts";
import type { TilePost } from "@/lib/types";

export async function getTilePosts(): Promise<TilePost[]> {
  const supabase = await createClient();
  const { data, error } = await supabase
    .from("tile_posts")
    .select(
      "id, tile_type, quantity, tile_type_other, tile_items, lat, lng, " +
      "post_type, contact, status, logistics, created_at, description, needs_mao_de_obra"
    )
    .order("created_at", { ascending: false });

  if (error) {
    console.error("getTilePosts error:", error);
    return [];
  }

  return (data ?? []).map(rowToTilePost);
}

Posting without an account

We didn’t want to force people to create an account just to post or edit. So we use a cookie-based creator token system: no login, but we still know which posts belong to which browser.

When someone publishes a post, we generate a random secret (e.g. a UUID), save it on the row, and store a mapping post ID → token in an httpOnly cookie. Only the server can read it, so the browser can’t be tricked into sending it elsewhere. No emails or passwords—just “these post IDs were created in this browser.”

When they come back to mark a post as claimed, update quantity, or delete it, the server reads the cookie, looks up the token for that post, and checks it against creator_token in the database. If it matches, we allow the update (using the service-role client, since RLS blocks anonymous updates). So people can post and manage their own listings without an account; only the browser that created a post can change or delete it.

Store the token on create:

// On create: one secret token per post, stored in DB and in httpOnly cookie
const token = crypto.randomUUID();
await supabase.from("tile_posts").insert({
  // ...tile_type, quantity, lat, lng, etc.
  creator_token: token,
}).select("id").single();
const existing = await getCreatorTokensCookie();
await setCreatorTokensCookie({
  ...existing,
  [id]: token, // postId → token so this browser can later edit this post
});

Verify before allowing edit or delete:

// Before any update/delete we verify: cookie token for this post === DB creator_token
const cookieTokens = await getCreatorTokensCookie();
const token = cookieTokens[postId];
if (!token) {
  return { success: false, error: "Only the author can mark as claimed." };
}
const supabase = await createClient();
const { data: post } = await supabase
  .from("tile_posts")
  .select("creator_token")
  .eq("id", postId)
  .single();
if (!post || post.creator_token !== token) {
  return { success: false, error: "Only the author can mark as claimed." };
}
// Verified: same browser that created the post. Use admin client to mutate (RLS blocks anon updates).
const admin = createAdminClient();
await admin.from("tile_posts").update({ status: "claimed" }).eq("id", postId);

Built by KORVO as a passion project to connect communities and support reconstruction after Storm Kristin.

Open app
                                                                                                      
                                                                        ░░▒▒▓▓▓▓▓▓▒▒                  
                                                                  ▒▒▓▓▒▒▓▓▓▓▓▓▒▒▒▒▓▓▓▓▓▓              
                                                    ░░▒▒▓▓▒▒▒▒▒▒▓▓▓▓▓▓████████▓▓▒▒▓▓████░░            
                                                ░░░░░░▒▒▒▒▒▒▒▒▒▒▓▓██████▓▓▓▓██▓▓▒▒▓▓▓▓████░░          
                                                ░░░░░░▒▒▒▒▒▒▓▓▓▓▓▓████████▓▓██▓▓▒▒▓▓▓▓██████▒▒        
                                          ░░░░▒▒▒▒▒▒▒▒▒▒▒▒░░░░░░▒▒▓▓████▓▓▓▓▓▓▓▓▓▓▒▒▓▓████████        
                                          ░░▒▒▒▒▓▓▓▓▓▓▒▒▒▒▓▓▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓████▓▓▒▒▓▓▓▓██████▓▓      
                                              ░░▒▒▓▓▓▓▓▓▒▒▒▒▒▒▓▓▓▓▓▓▓▓████████████▒▒▓▓▒▒▓▓██████      
                                                            ▒▒▓▓▓▓▓▓████▓▓██████▓▓▒▒▒▒▓▓▓▓██████▒▒    
                                                              ▓▓████████▓▓██████▓▓▓▓▒▒▓▓▓▓████████    
                                                              ▓▓████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████░░  
                                                              ▓▓▓▓████████████▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████▒▒  
                                                            ▓▓▓▓▓▓▓▓██▓▓████████▓▓▓▓▓▓▒▒▓▓▓▓▓▓████▓▓  
                                                        ▒▒▓▓▓▓▓▓▒▒▓▓▓▓▓▓████▓▓████▓▓▓▓▓▓▒▒▓▓▓▓▓▓██▓▓  
                                                      ▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓██▓▓▓▓██▓▓▓▓▓▓▓▓▓▓██████  
                                                    ██████▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████  
                                                ▓▓▓▓██████▓▓▓▓▒▒▓▓▒▒▒▒▓▓▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓██████  
                                            ░░████▓▓▓▓████▓▓▓▓▓▓▒▒▒▒▓▓▓▓▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓██████░░
                                          ░░██▓▓▓▓▓▓▒▒▓▓██▓▓▒▒▓▓▒▒▓▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓██████▓▓
                                          ██████▓▓▓▓▓▓▒▒▓▓▒▒▓▓▓▓▒▒▓▓▒▒▒▒▒▒▒▒▓▓▒▒▒▒▒▒▓▓▒▒▒▒▓▓▓▓▓▓██████
                                        ████▓▓████▓▓▓▓▓▓▓▓▓▓██▓▓▓▓▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▓▓▒▒▒▒▓▓██████
                                      ░░██▓▓████▓▓▓▓████▓▓██▓▓████▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓████████
                                      ██▓▓▓▓▓▓▓▓▓▓██████████▓▓████████████████████▓▓████████▓▓████████
                                    ░░▓▓▓▓▓▓▓▓████████████████████████████████████████████████████████
                                    ▓▓▓▓▓▓▓▓██████████████████████████████████████████████████████████
                                    ▓▓▓▓▓▓▓▓████████████████████████████████████████████████████████▒▒
                                  ▓▓▓▓▓▓▓▓████████████████████████████████████████████████████████▓▓  
                                  ██▓▓▓▓████████████████████████████████████████████████████████████░░
                                ▒▒▓▓▓▓██████████████████████████████████████████████████████████████  
                                ██▓▓▓▓██▓▓██████████████████████████████████████████████████████████  
                              ▒▒▓▓████████████████████████████████████████████████████████████████▒▒  
                              ▓▓▓▓▓▓▓▓▓▓██████████████████████████████████████████████████████████    
                            ▓▓▓▓▓▓██▓▓▓▓██▓▓██████████████████████████████████████████████████████    
                          ▒▒▓▓▓▓▓▓▓▓████▓▓██████████████████████████████████████████████████████      
                          ▓▓▓▓▓▓▓▓██████▓▓██████████████████████████████████████████████████████      
                        ▓▓▓▓▓▓▓▓▓▓██████▓▓██████████████████████████████████████████████████▓▓██      
                      ░░▓▓▓▓▓▓▓▓▓▓████████████████████████████████████████████████████████████▒▒      
                      ░░▓▓▓▓██▓▓██████████████████████████████████████████████████████████▓▓██        
                      ▒▒▓▓██▓▓████████████████████████████████████████████████████████████████        
                      ▓▓██▓▓▓▓▓▓████████████████████████████████████████████████████████▓▓████        
                    ██▓▓▓▓████████████████▓▓▓▓██████████████████████████████████████████████▓▓        
                  ░░▓▓▓▓▓▓▓▓██████████████▓▓██████████████████████████████████████████▓▓████░░        
                  ██▓▓████████████████████████████████████████████████████████████████▓▓████          
                ▒▒▓▓██████████████████████████████████████████████████████████████████████▓▓          
                ██▓▓████████████████████████████████████████████████████████████████▓▓▓▓▓▓▒▒          
              ▒▒▓▓██████████████████████████████████████████████████████████████▓▓▒▒  ▓▓▓▓░░          
              ▓▓██████████████████████████████████████████████████████████████░░  ▓▓  ██▓▓            
            ░░██████████████████████████████████████████████████████████▓▓        ▒▒  ▓▓██            
            ▓▓████████████████████████████████████████████▓▓▓▓  ▓▓██                  ▓▓░░            
          ░░██████████████████████████████████████▓▓▓▓▓▓░░      ▒▒██                  ▓▓              
          ████████████████████████████████████▓▓▓▓▓▓▒▒░░          ██░░                                
        ▓▓████████████████████████████████▓▓▓▓▒▒▒▒▓▓▓▓            ▓▓░░                                
      ▓▓██████████████████████████████████░░░░░░  ▓▓░░      ▒▒    ░░▓▓░░                              
    ████████▓▓██████████████████████████          ▓▓██        ▓▓    ▓▓▒▒                              
  ▒▒██████████████████████████████████            ░░██░░      ██░░  ▓▓▒▒                              
  ████████████████████████████  ████          ▒▒  ░░▓▓▓▓      ▒▒▓▓░░▒▒▓▓▓▓                            
▓▓██████▓▓██████████████████  ░░▓▓░░            ▓▓▓▓▓▓▒▒▒▒    ░░░░▒▒▒▒▒▒▓▓                            
  ▓▓▓▓██████████████████▒▒                      ░░▓▓▒▒▒▒▓▓▓▓░░  ░░░░░░▓▓▒▒▓▓                          
          ░░▒▒██████▒▒                              ▓▓▒▒▒▒▓▓▒▒▒▒░░░░░░▓▓▓▓▓▓▒▒                        
              ▓▓▓▓                                  ░░░░░░▒▒▒▒▒▒░░▒▒░░░░▒▒▓▓▓▓▒▒░░                    
                                                            ▒▒▒▒░░░░░░  ▒▒▒▒▒▒▒▒░░▒▒                  
                                                              ▒▒░░░░▒▒        ░░▒▒▒▒░░                

Ready to start your next sprint?

Contact

pedro@korvo.tech

+351 914 808 798

Leiria, Portugal

Social

  • LinkedIn
  • Instagram
  • X

Legal

  • Privacy Policy
  • Terms of Service