Add Supabase database schema and configuration#1
Conversation
- Initialize Supabase CLI config (config.toml) - Rename migration files to Supabase CLI timestamp format for compatibility - Add combined all_migrations.sql for easy one-step DB setup via SQL Editor - Update README with simplified setup instructions https://claude.ai/code/session_01JGosDHvo9xTpimALDPkZFn
📝 WalkthroughWalkthroughIntroduced comprehensive Supabase database infrastructure with a complete schema migration file, configuration settings for local Supabase development, associated ignore patterns, and updated documentation. The migration establishes multi-tenant support, full-text search, row-level security policies, analytics functions, and core business entities across organizational, CRM, deal, tenant, and financial domains. Changes
Poem
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@supabase/all_migrations.sql`:
- Around line 1099-1105: The computed column total_monthly_rent currently uses
coalesce(l.rent_amount, 0) + coalesce(l.cam_charges, 0) without adjusting for
lease frequency; update the SELECT expression that defines total_monthly_rent
(referencing l.rent_amount, l.cam_charges, and l.rent_frequency) to normalize
amounts to a monthly basis (e.g., divide by 3 for quarterly, by 12 for annual,
leave monthly as-is) using a CASE or similar expression and keep COALESCE around
rent_amount and cam_charges to avoid NULLs so the result correctly represents
monthly rent.
- Around line 631-639: The policies cc_select, cc_insert, cc_update, and
cc_delete on public.contact_companies only validate contacts.org_id; update each
policy to validate both sides of the junction by checking that the contact row
and the company row both belong to the caller's org (i.e., ensure exists(...)
for public.contacts where contacts.id = contact_id and contacts.org_id =
auth.org_id() AND exists(...) for public.companies where companies.id =
company_id and companies.org_id = auth.org_id()). Apply the same dual-side
validation to the other similar junction policy block referenced (lines
685-691).
- Around line 1219-1228: The query double-counts p.total_sf because properties
are joined to multiple active tenants; fix by aggregating per property before
summing: compute occupied square footage per property (e.g., sum(t.occupied_sf)
grouped by t.property_id or use max per property) and join that per-property
aggregate to public.properties so you sum each p.total_sf only once (or use
sum(distinct p.total_sf) if appropriate); update the SELECT block that
references public.properties p and public.tenants t so the denominator uses the
deduplicated property totals and the numerator uses the per-property occupied
aggregate (referencing p.total_sf, t.occupied_sf, and t.property_id to locate
the logic).
- Around line 880-886: The triggers (e.g., set_tenants_updated_at,
set_leases_updated_at) call a non-existent function public.set_updated_at();
replace those calls so they execute the actual function defined earlier,
public.update_updated_at(), i.e., change "execute function
public.set_updated_at()" to "execute function public.update_updated_at()" for
these triggers and for every other trigger in the script that currently
references public.set_updated_at().
- Around line 699-706: The policies "dfd_insert" and "dfd_update" on
public.deal_field_definitions currently allow org-level roles (auth.user_role()
in ('owner','admin')) to modify a global table; change those policies to
restrict writes to a system-level admin identity instead (e.g., replace the
condition with a system-only check such as auth.is_system_admin() OR
auth.user_role() = 'system_admin' depending on your auth helpers) so only
platform/system admins can INSERT/UPDATE the global deal_field_definitions while
leaving "dfd_select" unchanged; update the policy definitions named "dfd_insert"
and "dfd_update" accordingly and audit other global definition tables for
similar fixes.
- Around line 737-759: The audit table as created (public.audit_log) is writable
by any authenticated user via the "audit_insert" policy and has no triggers, so
it can be spoofed or left empty; fix by removing or tightening the
"audit_insert" policy so ordinary users cannot directly INSERT (e.g., allow only
auth.role() = 'service_role' or remove the policy entirely), and add a secure
trigger/function pair that writes canonical audit entries: implement a plpgsql
function (e.g., function audit.log_change() or audit_log_record()) that ON
INSERT/UPDATE/DELETE on target tables inserts into public.audit_log (capturing
action, table_name, record_id, old_data, new_data, user id from current_setting
or auth context), then create triggers (FOR EACH ROW) on the relevant tables to
call that function; ensure triggers run with a privileged role or rely on the
service role so only the trigger can populate public.audit_log and regular
clients cannot INSERT directly.
In `@supabase/config.toml`:
- Around line 154-156: Replace the IP-based and mismatched-scheme values in the
Supabase config by updating the site_url and additional_redirect_urls entries:
change site_url from "http://127.0.0.1:3000" to "http://localhost:3000" and
change additional_redirect_urls to use "http://localhost:3000" (remove the
https/127.0.0.1 variant) so Supabase Auth accepts the local redirect and the
scheme matches.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 1d262159-c10b-418c-9d36-52b786932c57
📒 Files selected for processing (28)
README.mdsupabase/.gitignoresupabase/all_migrations.sqlsupabase/config.tomlsupabase/migrations/20240101000001_extensions_and_functions.sqlsupabase/migrations/20240101000002_core_tables.sqlsupabase/migrations/20240101000003_contacts.sqlsupabase/migrations/20240101000004_properties.sqlsupabase/migrations/20240101000005_deals.sqlsupabase/migrations/20240101000006_future_schema.sqlsupabase/migrations/20240101000007_search_triggers.sqlsupabase/migrations/20240101000008_global_search.sqlsupabase/migrations/20240101000009_rls_policies.sqlsupabase/migrations/20240101000010_audit.sqlsupabase/migrations/20240101000011_seed_field_definitions.sqlsupabase/migrations/20240101000012_tenants_leases.sqlsupabase/migrations/20240101000013_operating_statements.sqlsupabase/migrations/20240101000014_capex_projects.sqlsupabase/migrations/20240101000015_debt_instruments.sqlsupabase/migrations/20240101000016_alter_documents.sqlsupabase/migrations/20240101000017_phase2_rls.sqlsupabase/migrations/20240101000018_views_functions.sqlsupabase/migrations/20240101000019_storage_bucket.sqlsupabase/migrations/20240101000020_task_enhancements.sqlsupabase/migrations/20240101000021_portfolio_kpi_functions.sqlsupabase/migrations/20240101000022_investors.sqlsupabase/migrations/20240101000023_investor_rls.sqlsupabase/migrations/20240101000024_notifications.sql
| -- Contact companies (junction -- derive from contact) | ||
| create policy "cc_select" on public.contact_companies for select | ||
| using (exists (select 1 from public.contacts where contacts.id = contact_id and contacts.org_id = auth.org_id())); | ||
| create policy "cc_insert" on public.contact_companies for insert | ||
| with check (exists (select 1 from public.contacts where contacts.id = contact_id and contacts.org_id = auth.org_id())); | ||
| create policy "cc_update" on public.contact_companies for update | ||
| using (exists (select 1 from public.contacts where contacts.id = contact_id and contacts.org_id = auth.org_id())); | ||
| create policy "cc_delete" on public.contact_companies for delete | ||
| using (exists (select 1 from public.contacts where contacts.id = contact_id and contacts.org_id = auth.org_id())); |
There was a problem hiding this comment.
Validate both sides of tenant-scoped junction rows.
These policies only prove ownership of one side of the relationship. A user who learns another org’s company_id or contact_id can create cross-tenant links, because the second foreign key is never checked against the caller’s org.
Also applies to: 685-691
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/all_migrations.sql` around lines 631 - 639, The policies cc_select,
cc_insert, cc_update, and cc_delete on public.contact_companies only validate
contacts.org_id; update each policy to validate both sides of the junction by
checking that the contact row and the company row both belong to the caller's
org (i.e., ensure exists(...) for public.contacts where contacts.id = contact_id
and contacts.org_id = auth.org_id() AND exists(...) for public.companies where
companies.id = company_id and companies.org_id = auth.org_id()). Apply the same
dual-side validation to the other similar junction policy block referenced
(lines 685-691).
| -- Deal field definitions (global, not org-scoped) | ||
| create policy "dfd_select" on public.deal_field_definitions for select | ||
| using (true); | ||
| create policy "dfd_insert" on public.deal_field_definitions for insert | ||
| with check (auth.user_role() in ('owner', 'admin')); | ||
| create policy "dfd_update" on public.deal_field_definitions for update | ||
| using (auth.user_role() in ('owner', 'admin')); | ||
|
|
There was a problem hiding this comment.
Don't let tenant admins write to a global definitions table.
deal_field_definitions is shared across every org, but any org owner or admin can insert/update it here. That gives one tenant write access to every tenant’s form schema.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/all_migrations.sql` around lines 699 - 706, The policies
"dfd_insert" and "dfd_update" on public.deal_field_definitions currently allow
org-level roles (auth.user_role() in ('owner','admin')) to modify a global
table; change those policies to restrict writes to a system-level admin identity
instead (e.g., replace the condition with a system-only check such as
auth.is_system_admin() OR auth.user_role() = 'system_admin' depending on your
auth helpers) so only platform/system admins can INSERT/UPDATE the global
deal_field_definitions while leaving "dfd_select" unchanged; update the policy
definitions named "dfd_insert" and "dfd_update" accordingly and audit other
global definition tables for similar fixes.
| -- Fallback: simple audit log table for tracking changes | ||
| create table public.audit_log ( | ||
| id uuid primary key default gen_random_uuid(), | ||
| org_id uuid references public.orgs(id) on delete cascade, | ||
| user_id uuid references auth.users(id), | ||
| action text not null check (action in ('insert', 'update', 'delete')), | ||
| table_name text not null, | ||
| record_id uuid not null, | ||
| old_data jsonb, | ||
| new_data jsonb, | ||
| created_at timestamptz not null default now() | ||
| ); | ||
|
|
||
| create index idx_audit_log_org on public.audit_log(org_id); | ||
| create index idx_audit_log_record on public.audit_log(table_name, record_id); | ||
| create index idx_audit_log_created on public.audit_log(created_at); | ||
|
|
||
| alter table public.audit_log enable row level security; | ||
|
|
||
| create policy "audit_select" on public.audit_log for select | ||
| using (org_id = auth.org_id()); | ||
| create policy "audit_insert" on public.audit_log for insert | ||
| with check (org_id = auth.org_id()); |
There was a problem hiding this comment.
The fallback audit log is neither automatic nor tamper-resistant.
There are no triggers in this migration that write to public.audit_log, and audit_insert lets authenticated users insert arbitrary rows for their org. That means this table can be empty or user-fabricated, so it cannot serve as a reliable audit trail.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/all_migrations.sql` around lines 737 - 759, The audit table as
created (public.audit_log) is writable by any authenticated user via the
"audit_insert" policy and has no triggers, so it can be spoofed or left empty;
fix by removing or tightening the "audit_insert" policy so ordinary users cannot
directly INSERT (e.g., allow only auth.role() = 'service_role' or remove the
policy entirely), and add a secure trigger/function pair that writes canonical
audit entries: implement a plpgsql function (e.g., function audit.log_change()
or audit_log_record()) that ON INSERT/UPDATE/DELETE on target tables inserts
into public.audit_log (capturing action, table_name, record_id, old_data,
new_data, user id from current_setting or auth context), then create triggers
(FOR EACH ROW) on the relevant tables to call that function; ensure triggers run
with a privileged role or rely on the service role so only the trigger can
populate public.audit_log and regular clients cannot INSERT directly.
| create trigger set_tenants_updated_at | ||
| before update on public.tenants | ||
| for each row execute function public.set_updated_at(); | ||
|
|
||
| create trigger set_leases_updated_at | ||
| before update on public.leases | ||
| for each row execute function public.set_updated_at(); |
There was a problem hiding this comment.
These triggers call a function that does not exist.
The migration defines update_updated_at() earlier, but every trigger here executes public.set_updated_at(). The consolidated script will stop at the first one, so the rest of the schema never gets created.
Suggested fix
- for each row execute function public.set_updated_at();
+ for each row execute function public.update_updated_at();Also applies to: 912-914, 945-947, 989-991, 1014-1016, 1181-1183, 1322-1324, 1350-1352, 1379-1381, 1405-1407
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/all_migrations.sql` around lines 880 - 886, The triggers (e.g.,
set_tenants_updated_at, set_leases_updated_at) call a non-existent function
public.set_updated_at(); replace those calls so they execute the actual function
defined earlier, public.update_updated_at(), i.e., change "execute function
public.set_updated_at()" to "execute function public.update_updated_at()" for
these triggers and for every other trigger in the script that currently
references public.set_updated_at().
| l.lease_type, | ||
| l.start_date, | ||
| l.end_date, | ||
| l.rent_amount, | ||
| l.rent_frequency, | ||
| l.cam_charges, | ||
| coalesce(l.rent_amount, 0) + coalesce(l.cam_charges, 0) as total_monthly_rent, |
There was a problem hiding this comment.
Normalize lease frequency before exposing total_monthly_rent.
rent_frequency allows quarterly and annual leases, but total_monthly_rent is calculated as a raw rent_amount + cam_charges. That overstates non-monthly leases and will skew rent-roll totals.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/all_migrations.sql` around lines 1099 - 1105, The computed column
total_monthly_rent currently uses coalesce(l.rent_amount, 0) +
coalesce(l.cam_charges, 0) without adjusting for lease frequency; update the
SELECT expression that defines total_monthly_rent (referencing l.rent_amount,
l.cam_charges, and l.rent_frequency) to normalize amounts to a monthly basis
(e.g., divide by 3 for quarterly, by 12 for annual, leave monthly as-is) using a
CASE or similar expression and keep COALESCE around rent_amount and cam_charges
to avoid NULLs so the result correctly represents monthly rent.
| select case | ||
| when coalesce(sum(p.total_sf), 0) = 0 then 0 | ||
| else round( | ||
| coalesce(sum(t.occupied_sf), 0) / sum(p.total_sf) * 100, | ||
| 1 | ||
| ) | ||
| end | ||
| from public.properties p | ||
| left join public.tenants t on t.property_id = p.id and t.status = 'active' | ||
| where p.org_id = auth.org_id(); |
There was a problem hiding this comment.
The occupancy denominator is duplicated per active tenant.
This query joins properties directly to tenants and then sums p.total_sf, so a property with multiple active tenants contributes its total square footage multiple times. Occupancy will be understated for multi-tenant assets.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/all_migrations.sql` around lines 1219 - 1228, The query
double-counts p.total_sf because properties are joined to multiple active
tenants; fix by aggregating per property before summing: compute occupied square
footage per property (e.g., sum(t.occupied_sf) grouped by t.property_id or use
max per property) and join that per-property aggregate to public.properties so
you sum each p.total_sf only once (or use sum(distinct p.total_sf) if
appropriate); update the SELECT block that references public.properties p and
public.tenants t so the denominator uses the deduplicated property totals and
the numerator uses the per-property occupied aggregate (referencing p.total_sf,
t.occupied_sf, and t.property_id to locate the logic).
| site_url = "http://127.0.0.1:3000" | ||
| # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. | ||
| additional_redirect_urls = ["https://127.0.0.1:3000"] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find and examine README.md to verify documented app URL
fd -t f "README" . | head -10Repository: JTVOLPS/conca
Length of output: 68
🏁 Script executed:
# Check the supabase/config.toml file at the specified lines
cat -n supabase/config.toml | sed -n '150,160p'Repository: JTVOLPS/conca
Length of output: 680
🏁 Script executed:
# Search for all references to localhost and 127.0.0.1 in the codebase
rg "localhost:3000|127\.0\.0\.1:3000" --type-list | head -20Repository: JTVOLPS/conca
Length of output: 446
🏁 Script executed:
# Search for environment configuration and app URL setup
rg "NEXT_PUBLIC_APP_URL|APP_URL" --type-list | head -20Repository: JTVOLPS/conca
Length of output: 446
🏁 Script executed:
# Read README.md to check documented app URL
cat README.mdRepository: JTVOLPS/conca
Length of output: 4919
🏁 Script executed:
# Search for NEXT_PUBLIC_APP_URL and app URL references
rg "NEXT_PUBLIC_APP_URL|localhost:3000|app.*url" -iRepository: JTVOLPS/conca
Length of output: 2029
🏁 Script executed:
# Check for environment setup files
fd -t f "\.env|env\.example|\.env\.local" .Repository: JTVOLPS/conca
Length of output: 77
🏁 Script executed:
# Search for any Supabase auth configuration or references to redirect matching
rg "redirect|localhost|127\.0\.0\.1" supabase/ -iRepository: JTVOLPS/conca
Length of output: 796
🏁 Script executed:
# Check .env.local.example for complete configuration
cat .env.local.exampleRepository: JTVOLPS/conca
Length of output: 407
🌐 Web query:
Supabase Auth redirect URL matching localhost vs 127.0.0.1 behavior
💡 Result:
Supabase Auth treats redirect URLs with "localhost" and "127.0.0.1" differently due to validation rules in the auth server that reject IP addresses (like 127.0.0.1) for security reasons, while allowing "localhost" for local development. Key points: - Official docs always use and recommend "http://localhost:3000/" (or similar) for local dev redirects, never 127.0.0.1. - In GitHub issue #36640 (2025), a user reported redirect issues with an IP address in redirect URLs; replacing it with "localhost" fixed it. Comment references code in supabase/auth/internal/utilities/request.go (lines 103-108) validating redirects. - PR #1984 (merged April 2025, title "fix: redirects must not be to ip addresses") explicitly enforces no IP addresses in redirects. - Reddit thread reports mismatches when app binds to 127.0.0.1 but Site URL uses localhost, causing CORS or redirect failures; solution is matching exactly, preferring localhost. - Matching must be exact (scheme, host, path); wildcards like / allowed, but host must pass validation. Behavior summary: - http://localhost:PORT/ : Allowed and works for local dev. - http://127.0.0.1:PORT/ : Rejected by validation as IP address. - Use window.location.origin carefully; if browser resolves to 127.0.0.1 but config is localhost, mismatch fails. - For self-hosted/local Supabase, same rules apply via config.toml. To avoid issues: Always configure Site URL and additional redirects with "localhost" for local dev, use dynamic origin detection that prefers localhost.
Citations:
- 1: https://supabase.com/docs/guides/auth/redirect-urls
- 2: https://www.reddit.com/r/Supabase/comments/1efj697/why_do_i_keep_getting_redirected_to/
- 3: https://supabase.com/docs/guides/troubleshooting/why-am-i-being-redirected-to-the-wrong-url-when-using-auth-redirectto-option-_vqIeO
- 4: Localhost redirect URLs not respected in resetPasswordForEmail supabase/supabase#36640
- 5: fix: redirects must not be to ip addresses supabase/auth#1984
Auth configuration blocks local development—Supabase rejects 127.0.0.1 redirects.
The config uses http://127.0.0.1:3000 as the site URL and https://127.0.0.1:3000 for redirects, but Supabase Auth explicitly rejects IP addresses in redirect URLs (see PR supabase/auth#1984). Additionally, the scheme mismatch (http vs https) will cause validation to fail. The README and .env.local.example correctly document http://localhost:3000, which is what Supabase Auth accepts for local development.
Update both site_url and additional_redirect_urls to use http://localhost:3000:
Suggested fix
-site_url = "http://127.0.0.1:3000"
+site_url = "http://localhost:3000"
-additional_redirect_urls = ["https://127.0.0.1:3000"]
+additional_redirect_urls = ["http://localhost:3000"]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@supabase/config.toml` around lines 154 - 156, Replace the IP-based and
mismatched-scheme values in the Supabase config by updating the site_url and
additional_redirect_urls entries: change site_url from "http://127.0.0.1:3000"
to "http://localhost:3000" and change additional_redirect_urls to use
"http://localhost:3000" (remove the https/127.0.0.1 variant) so Supabase Auth
accepts the local redirect and the scheme matches.


Summary
This PR introduces the complete Supabase database schema and local development configuration for the real estate deal management platform. It consolidates all migration files into a single comprehensive schema file and adds Supabase CLI configuration.
Key Changes
Consolidated Migration File: Created
supabase/all_migrations.sqlcontaining all 24 migrations in a single file for easier management and deploymentSupabase Configuration: Added
supabase/config.tomlwith complete local development setup including:Database Schema: Comprehensive schema includes:
Security & Access Control:
auth.org_id()helper functionauth.user_role()helperSearch & Indexing:
Data Integrity:
updated_attimestamp management via triggersGit Configuration: Added
.gitignorefor Supabase local development artifactsDocumentation: Updated README.md with simplified setup instructions referencing the consolidated migration file
Notable Implementation Details
20240101000001prefix for proper orderinghttps://claude.ai/code/session_01JGosDHvo9xTpimALDPkZFn
Summary by CodeRabbit
Documentation
New Features
Chores