Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 39 additions & 20 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,49 @@
## 🔗 Related Issue
Closes #issue-number
## Summary

---
Closes #[issue number]

## 🔖 Title
<!-- Brief and clear. Describe the specific task -->
Briefly describe what this PR does in 2-3 sentences.

---
## This repo is for the NestJS backend API only

## 📝 Description
<!-- Describe the changes made in this PR. What problem does it solve? -->
Before submitting, confirm your changes belong here:

---
- [ ] My changes are inside src/ or test/
- [ ] I have NOT added React, React Native,
or frontend component files
- [ ] I have NOT added Rust or Soroban contract code
- [ ] This is NestJS/TypeScript backend work

## 🔄 Changes Made
<!-- List the main changes in this PR -->
- [ ] <!-- Change 1 -->
- [ ] <!-- Change 2 -->
- [ ] <!-- Change 3 -->
## Type of change

---
- [ ] Bug fix
- [ ] New endpoint
- [ ] New service or module
- [ ] Database migration
- [ ] Background job
- [ ] Test coverage

## 📸 Screenshots (if applicable)
<!-- Add screenshots or GIFs if the changes affect the UI -->
## Testing

---
- [ ] npm run build passes with zero TypeScript errors
- [ ] npm test passes — all 184+ existing tests pass
- [ ] No new `any` types introduced anywhere
- [ ] Swagger decorators added to every new endpoint
- [ ] Migration file created for any schema changes
- [ ] New unit tests written for new service methods

## 🗒️ Additional Notes
<!-- Any additional information, concerns, or context for reviewers -->
## Context files reviewed

- [ ] context/architecture-context.md
- [ ] context/code-standards.md
- [ ] context/progress-tracker.md updated

## Mandatory before requesting review

Running these must all exit 0:
npm run build
npm test

If either fails, fix it before opening this PR.
PRs with failing CI checks will be closed without review.
PRs that reduce the test count will be rejected.
32 changes: 32 additions & 0 deletions src/database/supabase.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,36 @@ export class SupabaseService {
getServiceRoleClient(): SupabaseClient {
return this.serviceRoleClient;
}

/**
* Creates a wallet-scoped client for RLS enforcement.
* Uses anon key (non-service-role) which respects RLS policies.
*
* IMPORTANT: Before using this client, the caller must execute:
* SET LOCAL app.current_wallet = 'G...' via raw SQL
* This can be done using client.rpc() with a custom function or
* by wrapping queries in a transaction that sets the variable.
*
* Example usage:
* const client = this.getWalletScopedClient();
* await client.rpc('set_app_current_wallet', { wallet: walletAddress });
* // Now queries respect RLS
*/
getWalletScopedClient(): SupabaseClient {
return createClient(
this.configService.get<string>('SUPABASE_URL'),
this.configService.get<string>('SUPABASE_ANON_KEY'),
{
auth: {
persistSession: false,
autoRefreshToken: false,
detectSessionInUrl: false,
},
realtime: {
transport: ws as unknown as typeof WebSocket,
},
},
);
}

}
145 changes: 145 additions & 0 deletions supabase/migrations/20260718000000_rls_policies.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
-- Row Level Security Policies for StepFi Tables
-- This migration adds RLS policies to ensure users can only access their own data
-- Service role bypasses RLS automatically for admin operations
--
-- IMPORTANT: These policies require the API layer to execute
-- SET LOCAL app.current_wallet = 'G...' before each query using a
-- non-service-role client. The current API uses service-role client which
-- bypasses RLS entirely. To enable RLS enforcement, the API layer must:
-- 1. Use anon-key client (non-service-role) for user-facing queries
-- 2. Execute SET LOCAL app.current_wallet = $wallet via raw SQL before queries

-- ============================================================================
-- Enable RLS on tables (ensure it's enabled)
-- ============================================================================
ALTER TABLE public.learner_profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.vouches ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.vendors ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.sponsor_pools ENABLE ROW LEVEL SECURITY;

-- ============================================================================
-- RPC function to set session variable for RLS
-- ============================================================================
CREATE OR REPLACE FUNCTION public.set_app_current_wallet(wallet TEXT)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
PERFORM set_config('app.current_wallet', wallet, false);
END;
$$;

-- Grant execute to authenticated users
GRANT EXECUTE ON FUNCTION public.set_app_current_wallet(TEXT) TO anon;
GRANT EXECUTE ON FUNCTION public.set_app_current_wallet(TEXT) TO authenticated;

-- ============================================================================
-- learner_profiles
-- ============================================================================
-- Users can read their own profile
CREATE POLICY "Users can read own learner profile"
ON public.learner_profiles
FOR SELECT
USING (wallet_address = current_setting('app.current_wallet', true)::TEXT);

-- Users can insert their own profile
CREATE POLICY "Users can insert own learner profile"
ON public.learner_profiles
FOR INSERT
WITH CHECK (wallet_address = current_setting('app.current_wallet', true)::TEXT);

-- Users can update their own profile
CREATE POLICY "Users can update own learner profile"
ON public.learner_profiles
FOR UPDATE
USING (wallet_address = current_setting('app.current_wallet', true)::TEXT);

-- ============================================================================
-- loans
-- ============================================================================
-- Users can read their own loans
CREATE POLICY "Users can read own loans"
ON public.loans
FOR SELECT
USING (user_wallet = current_setting('app.current_wallet', true)::TEXT);


-- ============================================================================
-- vouches
-- ============================================================================
-- Users can read vouches where they are either mentor or learner
CREATE POLICY "Users can read own vouches"
ON public.vouches
FOR SELECT
USING (
mentor_wallet = current_setting('app.current_wallet', true)::TEXT
OR
learner_wallet = current_setting('app.current_wallet', true)::TEXT
);

-- Users can insert vouches where they are the mentor
CREATE POLICY "Mentors can insert vouches"
ON public.vouches
FOR INSERT
WITH CHECK (mentor_wallet = current_setting('app.current_wallet', true)::TEXT);

-- Users can update vouches where they are the mentor
CREATE POLICY "Mentors can update own vouches"
ON public.vouches
FOR UPDATE
USING (mentor_wallet = current_setting('app.current_wallet', true)::TEXT);

-- ============================================================================
-- vendors (formerly merchants)
-- ============================================================================
-- Authenticated users can read all vendors (public data)
CREATE POLICY "Authenticated users can read vendors"
ON public.vendors
FOR SELECT
USING (true);


-- ============================================================================
-- liquidity_positions (pool_positions)
-- ============================================================================
-- Users can read their own liquidity positions
CREATE POLICY "Users can read own liquidity positions"
ON public.liquidity_positions
FOR SELECT
USING (provider_wallet = current_setting('app.current_wallet', true)::TEXT);


-- ============================================================================
-- sponsor_pools
-- ============================================================================
-- Users can read their own sponsor pool
CREATE POLICY "Users can read own sponsor pool"
ON public.sponsor_pools
FOR SELECT
USING (wallet_address = current_setting('app.current_wallet', true)::TEXT);

-- Users can insert their own sponsor pool
CREATE POLICY "Users can insert own sponsor pool"
ON public.sponsor_pools
FOR INSERT
WITH CHECK (wallet_address = current_setting('app.current_wallet', true)::TEXT);

-- Users can update their own sponsor pool
CREATE POLICY "Users can update own sponsor pool"
ON public.sponsor_pools
FOR UPDATE
USING (wallet_address = current_setting('app.current_wallet', true)::TEXT);

-- ============================================================================
-- Notes
-- ============================================================================
-- 1. Service role bypasses RLS automatically for all operations
-- 2. Non-service-role clients must execute SET LOCAL app.current_wallet = 'G...'
-- via raw SQL before each query for RLS enforcement
-- 3. No blanket-permissive policies (WITH CHECK (true)) - service role bypasses RLS
-- 4. repayment_installments table does not exist - payment_index is used instead
-- (payment_index is an indexer table managed by service role)
-- 5. pool_positions is implemented as liquidity_positions
-- 6. RLS is explicitly enabled on all target tables in this migration