ghrepocloneMin11Benja/alertagas-twilio-smscdalertagas-twilio-smsnpminstallcp.env.example.env.local# Fill environment variables (ask team lead)npmrundev
Twilio SMS → Webhook API → Supabase DB → Next.js Frontend → Real-time Updates
# Using GitHub CLI (recommended)
gh repo clone Min11Benja/alertagas-twilio-sms
cd alertagas-twilio-sms
# Using Git
git clone https://github.com/Min11Benja/alertagas-twilio-sms.git
cd alertagas-twilio-sms
# Install dependencies
npm install
cp .env.example .env.local
# Twilio (get from Twilio Console)
TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
TWILIO_AUTH_TOKEN=your_auth_token_here
# Supabase (get from Project Settings → API)
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOi...
SUPABASE_SERVICE_ROLE_KEY=eyJ0eXAiOiJKV1QiLCJhbGciOi... # 🚨 SERVER ONLY
# Resend (Email Notifications - Optional)
RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxx
# Optional: Google Maps
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=your_maps_api_key
-- ============================================
-- ALERTAGAS — CLEAN DATABASE SCHEMA SETUP
-- For a new Supabase project (no prior objects)
-- ============================================
-- Extensions (for UUIDs, text search)
create extension if not exists pgcrypto;
create extension if not exists pg_trgm;
-- ============================================
-- ENUMS
-- ============================================
do $$ begin
create type public.message_type as enum ('reading','heartbeat','battery','alert','other');
exception when duplicate_object then null; end $$;
do $$ begin
create type public.battery_status as enum ('UNKNOWN','LOW','MEDIUM','HIGH','CHARGING');
exception when duplicate_object then null; end $$;
do $$ begin
create type public.sms_status as enum ('received','parsed','linked','ignored','error');
exception when duplicate_object then null; end $$;
-- ============================================
-- TABLE: clients
-- ============================================
create table if not exists public.clients (
id uuid primary key default gen_random_uuid(),
name text not null,
phone_number text,
email text,
contact_number text,
address text,
latitude numeric,
longitude numeric,
created_at timestamptz default now()
);
-- ============================================
-- TABLE: profiles
-- ============================================
create table if not exists public.profiles (
user_id uuid primary key references auth.users(id) on delete cascade,
name text,
email text unique,
client_id uuid references public.clients(id) on delete set null,
role text default 'user',
created_at timestamptz default now(),
updated_at timestamptz default now()
);
create index if not exists idx_profiles_client_id on public.profiles(client_id);
-- ============================================
-- TABLE: sensors
-- ============================================
create table if not exists public.sensors (
id uuid primary key default gen_random_uuid(),
sensor_name text,
phone_number text not null unique,
normalized_phone text,
client_id uuid references public.clients(id) on delete set null,
created_at timestamptz default now(),
fill_level numeric,
status text default 'active',
last_updated timestamptz default now(),
address text,
threshold_percent integer default 20
);
create unique index if not exists uidx_sensors_normalized_phone
on public.sensors (coalesce(normalized_phone, phone_number));
create index if not exists idx_sensors_client_id on public.sensors(client_id);
-- ============================================
-- TABLE: sms_messages
-- ============================================
create table if not exists public.sms_messages (
id uuid primary key default gen_random_uuid(),
from_number text not null,
normalized_from text,
body text not null,
received_at timestamptz default now(),
sensor_id uuid references public.sensors(id) on delete set null,
client_id uuid references public.clients(id) on delete set null,
status public.sms_status default 'received',
message_type public.message_type default 'other',
gas_level integer,
battery public.battery_status default 'UNKNOWN',
raw_payload jsonb,
is_linked boolean default false,
dedupe_key text,
dedupe_hash text
);
create index if not exists idx_sms_messages_received_at on public.sms_messages(received_at desc);
create index if not exists idx_sms_messages_sensor_id on public.sms_messages(sensor_id);
create index if not exists idx_sms_messages_client_id on public.sms_messages(client_id);
create index if not exists idx_sms_messages_from_trgm on public.sms_messages using gin (from_number gin_trgm_ops);
create unique index if not exists uidx_sms_dedupe_key on public.sms_messages(dedupe_key);
create unique index if not exists uidx_sms_dedupe_hash on public.sms_messages(dedupe_hash);
-- ============================================
-- TABLE: gas_readings
-- ============================================
create table if not exists public.gas_readings (
id uuid primary key default gen_random_uuid(),
sensor_id uuid not null references public.sensors(id) on delete cascade,
client_id uuid not null references public.clients(id) on delete cascade,
gas_level integer not null check (gas_level between 0 and 100),
status text not null check (status in ('Low','Normal','High')),
location jsonb,
reading numeric check (reading is null or reading >= 0),
recorded_at timestamptz default now()
);
create index if not exists idx_readings_sensor_time on public.gas_readings(sensor_id, recorded_at desc);
create index if not exists idx_readings_client_time on public.gas_readings(client_id, recorded_at desc);
-- ============================================
-- TABLE: alerts
-- ============================================
create table if not exists public.alerts (
id uuid primary key default gen_random_uuid(),
sensor_id uuid not null references public.sensors(id) on delete cascade,
alert_type text not null,
message text not null,
status text default 'new',
created_at timestamptz default now(),
resolved_at timestamptz
);
create index if not exists idx_alerts_sensor_time on public.alerts(sensor_id, created_at desc);
-- ============================================
-- ROW LEVEL SECURITY (RLS)
-- ============================================
alter table public.clients enable row level security;
alter table public.sensors enable row level security;
alter table public.sms_messages enable row level security;
alter table public.gas_readings enable row level security;
alter table public.alerts enable row level security;
alter table public.profiles enable row level security;
-- Drop any existing policies to avoid duplicates
do $$
declare p record;
begin
for p in select policyname, tablename from pg_policies where schemaname='public' loop
execute format('drop policy if exists %I on public.%I;', p.policyname, p.tablename);
end loop;
end $$;
-- ============================================
-- POLICIES (RLS)
-- ============================================
create policy profiles_self_select
on public.profiles for select
using (auth.uid() = user_id);
create policy profiles_self_update
on public.profiles for update
using (auth.uid() = user_id);
create policy clients_read_own
on public.clients for select
using (id = (select client_id from public.profiles where user_id = auth.uid()));
create policy sensors_read_own
on public.sensors for select
using (client_id = (select client_id from public.profiles where user_id = auth.uid()));
create policy sms_read_own
on public.sms_messages for select
using (client_id = (select client_id from public.profiles where user_id = auth.uid()));
create policy readings_read_own
on public.gas_readings for select
using (client_id = (select client_id from public.profiles where user_id = auth.uid()));
create policy alerts_read_own
on public.alerts for select
using (sensor_id in (
select id from public.sensors
where client_id = (select client_id from public.profiles where user_id = auth.uid())
));
-- ============================================
-- ✅ FINISHED
-- Verify everything
-- ============================================
select '✅ AlertaGas schema created successfully!' as status;
-- Check tables were created
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;
-- Verify RLS is enabled
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public';
# Start development server
npm run dev
# Expose localhost (in separate terminal)
npx ngrok http 3000
# Start development server
npm run dev
# Run in different terminals as needed
npm run lint:watch # Watch mode for ESLint
npm run typecheck # TypeScript validation
# Check environment variables are loaded
npm run validate:env
# Inspect ngrok traffic
http://127.0.0.1:4040 # ngrok web interface
# Database inspection
npx supabase status # Verify local Supabase connection
# Daily development commands
npm run dev # Start development
npm run lint # Check code quality
npm run typecheck # Verify types
npm run build # Production build test
# Deployment
git push origin main # Triggers auto-deploy
# Testing
curl -X POST https://your-app.ngrok.io/api/receive-sms \
-d "From=+1234567890&Body=Test%20Message"