Skip to content

← Back to DWS Receipts

DWS Receipts uses Supabase (Postgres) with three tables, Row Level Security, and three client configurations for different access levels.

For client-side React components.

lib/supabaseClient.ts
import { createBrowserClient } from '@supabase/ssr';
export const supabase = createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

For API routes with cookie-based auth.

lib/supabaseServerClient.ts
import { createServerClient } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createSupabaseServerClient() {
const cookieStore = await cookies();
return createServerClient(url, anonKey, {
cookies: { get, set, remove } // Cookie handlers
});
}

Session extended to 6 months via custom cookie options.

For privileged operations bypassing RLS.

lib/supabaseAdminClient.ts
import 'server-only'; // Prevents client-side import
import { createClient } from '@supabase/supabase-js';
export const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{ auth: { autoRefreshToken: false, persistSession: false } }
);
ColumnTypeDescription
iduuidPrimary key
user_iduuidFK → auth.users
receipt_datedateReceipt date
amountnumericDollar amount
statustextPending/Approved/Rejected/Reimbursed
category_iduuidFK → categories
descriptiontextUser notes
image_urltextStorage path
created_attimestampAuto-generated
updated_attimestampAuto-updated

RLS: Enabled. Users can only access their own receipts (unless admin).

ColumnTypeDescription
iduuidPrimary key
nametextCategory name (unique)
created_attimestampAuto-generated

RLS: Disabled (public data).

Default Categories: Parking, Gas, Meals & Entertainment, Office Supplies, Other

ColumnTypeDescription
user_iduuidPK, FK → auth.users
roletext’employee’ or ‘admin’
full_nametextFull name
preferred_nametextDisplay name
employee_id_internaltextInternal ID
created_attimestampAuto-generated
updated_attimestampAuto-updated
deleted_attimestampSoft delete

RLS: Enabled.

lib/types.ts
interface Receipt {
id: string;
user_id?: string;
employeeName: string; // Derived from profile
employeeId: string; // Derived from profile
phone?: string; // From auth.users
date: string; // Frontend field
receipt_date?: string; // Database field
amount: number;
status: "Pending" | "Approved" | "Rejected" | "Reimbursed";
category_id?: string;
category?: string; // Display name from join
description?: string;
notes?: string; // Alias for description
image_url?: string;
}
interface UserProfile {
user_id: string;
role: 'employee' | 'admin';
full_name?: string;
preferred_name?: string;
employee_id_internal?: string;
}
interface Category {
id: string;
name: string;
}

For admin operations needing phone numbers (in auth.users):

-- Postgres RPC function
CREATE FUNCTION get_admin_receipts_with_phone(...)
RETURNS TABLE(...)
SECURITY DEFINER -- Runs with elevated privileges
AS $$
SELECT r.*, au.phone
FROM receipts r
JOIN auth.users au ON r.user_id = au.id
...
$$;

Called via: supabase.rpc('get_admin_receipts_with_phone', { ... })

Bucket: receipt-images

Path Structure:

{user_id}/temp_{uuid}_{timestamp}.jpg # Temporary
{user_id}/{receipt_id}.jpg # Final

Public URL Generation:

const { data } = supabase.storage
.from('receipt-images')
.getPublicUrl(image_url);
const { data, error } = await supabase
.from('receipts')
.insert({ user_id, receipt_date, amount, ... })
.select()
.single();
const { data } = await supabase
.from('receipts')
.select(`
*,
categories!receipts_category_id_fkey (name)
`)
.eq('user_id', userId)
.order('receipt_date', { ascending: false });
const { data } = await supabase
.from('receipts')
.update({ status: 'Approved', updated_at: new Date().toISOString() })
.eq('id', receiptId)
.select();