Sell globally. Grow your revenue.
Sell globally. Grow your revenue. Card and monthly payments for freelancers, agencies and SaaS from 190+ countries, with tax and invoices handled. No company needed.
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
A digital chartering & ship-brokering platform that connects cargo owners, vessel owners, and brokers — letting them list cargoes and vessel positions, discover counterparties through an authoritative matching engine, run voyage economics (P&L / TCE, Ports DA, Suez Canal toll), and consume a weekly market-insights edition. Counterparty contact details (the broker's core asset) are firewalled at the database layer so they are never leaked through the API or UI.
Built with Next.js 16 (App Router) + React 19, Supabase (Postgres + Auth + RLS), and an optional Anthropic-powered document parser.查看完整 README ↗A digital chartering & ship-brokering platform that connects cargo owners, vessel owners, and brokers — letting them list cargoes and vessel positions, discover counterparties through an authoritative matching engine, run voyage economics (P&L / TCE, Ports DA, Suez Canal toll), and consume a weekly market-insights edition. Counterparty contact details (the broker's core asset) are firewalled at the database layer so they are never leaked through the API or UI.
Built with Next.js 16 (App Router) + React 19, Supabase (Postgres + Auth + RLS), and an optional Anthropic-powered document parser.
New to this codebase? Start with ONBOARDING.md — it covers the current state (including known code↔DB drift you must verify against the live schema), the Data Sync ingestion pipeline, and the non-obvious gotchas. This README documents the intended architecture; ONBOARDING documents today's reality.
Arab ShipBroker is a multi-tenant maritime marketplace and broker workbench. Three counterparty roles operate inside one portal:
The platform's defining constraint is the contact firewall: a broker's value lives in who-knows-whom, so counterparty PII (owner/manager names, emails, phones) is never exposed to other members — enforced in Postgres via Row-Level Security, masked views, and SECURITY DEFINER RPCs, with a reproducible proof harness.
A public marketing site (home, services, market insights, contact) sits in front of an authenticated portal (/dashboard/*) and an admin console (/admin/*).
/dashboard)/admin)/admin/data-sync) — bulk-ingest the CargoMap workbook (.xlsx): upload → stage & diff → review (with row-by-row selective sync) → reversible commit. Includes a Manual Review area (unmapped commodities, IMO-less vessels, and an invalid-rows "Needs fixing" queue by category) and a "Post open positions" action that turns 02_VESSELS open rows into vessel availability. This is the primary data-loading path — see ONBOARDING.md §6./api/cron/market-insights, Mondays 06:00) calls a SECURITY DEFINER generator RPC that freezes an immutable, dated edition.| Concern | Technology |
|---|---|
| Framework | Next.js 16 (App Router, RSC, route groups) |
| UI library | React 19 |
| Styling | Tailwind CSS v4 (@tailwindcss/postcss), tw-animate-css |
| Components | shadcn/ui + Radix UI primitives, class-variance-authority, tailwind-merge, clsx |
| Icons | lucide-react |
| Animation | framer-motion, nextjs-toploader |
| Forms & validation | react-hook-form + zod (@hookform/resolvers) |
| Maps | Leaflet + leaflet.markercluster |
| Notifications | sonner (toasts) |
| Spreadsheets | xlsx (data import/export) |
| Concern | Technology |
|---|---|
| Database | Supabase Postgres (RLS, views, RPCs, enums, triggers) |
| Auth | Supabase Auth (@supabase/ssr, @supabase/supabase-js) |
| Server logic | Next.js Route Handlers + Server Actions (Node runtime) |
| AI parsing | Anthropic SDK (@anthropic-ai/sdk, Claude Opus) — optional |
| Scheduling | Vercel Cron (vercel.json) |
| Tooling | TypeScript 5, ESLint 9 (eslint-config-next), tsx for scripts, Python (seed generation) |
┌──────────────────────────────────────────────────────────────────┐
│ Browser │
│ Public site · Member portal (/dashboard) · Admin (/admin) │
└───────────────┬───────────────────────────────────┬──────────────┘
│ HTTPS │
┌───────▼─────────────────────────────────────▼──────────────┐
│ Next.js 16 on Vercel (Edge + Node) │
│ │
│ middleware.ts → auth gate, email-verify gate, │
│ role-based routing, suspension check │
│ │
│ Route groups: (public) (auth) (dashboard) (admin) │
│ Server Components / Server Actions ─ lib/portal/actions │
│ Route Handlers /api/* (circulars, ports, bunker, cron) │
│ SDK layer sdk/* (typed data access over Supabase) │
└───────┬───────────────────────────────────────┬────────────┘
│ anon key (RLS-scoped) │ service-role
│ │ (server-only)
┌───────▼─────────────────────────────────────────▼──────────┐
│ Supabase Postgres │
│ RLS policies · masked views (v_vessel_detail, cargos_*) │
│ SECURITY DEFINER RPCs (matching, stats, market insights) │
│ Contact firewall (PII admin/owner-only) │
└─────────────────────────────────────────────────────────────┘
│
┌───────▼────────────┐ ┌──────────────────────────────┐
│ Anthropic API │ │ Vercel Cron (weekly) │
│ (circular parser) │ │ → /api/cron/market-insights │
└─────────────────────┘ └──────────────────────────────┘
(public), (auth), (dashboard), (admin) keep layouts, access rules, and shells isolated while sharing one Next.js app.browser.ts — client components (anon key, RLS-scoped).server.ts — server components/actions (cookie-bound session, anon key).admin.ts — service role, server-only, bypasses RLS for trusted operations (signup, account deletion, cron). Never imported into client code.SECURITY DEFINER RPCs. Client-side logic (lib/portal/matching.ts) only mirrors the same hard gates for instant surfaces (map/top-matches); authoritative counts come from the DB RPCs (get_matches_for_cargo / get_matches_for_availability).v_vessel_detail, cargos_access_view, column grants), proven by a self-contained 12-check harness, not merely hidden in the UI.public.users.id ≠ auth.uid(); lookups resolve via supabase_user_id first to avoid login bounces.seed/unified_dataset.sql loads the real dataset (276 ports, 88 vessels, 27 open positions, 719 cargo).app/ # App Router — route groups (public, auth, dashboard, admin) + /api
components/ # UI by domain (admin, portal, cargo, vessels, ui, market-insights …)
contexts/ # React context (DashboardContext — account + sidebar state)
lib/ # Domain logic: portal/ (matching, econ, types), supabase/, schemas/, admin/
sdk/ # Typed data-access layer over Supabase (auth, cargos, vessels, ports …)
supabase/ # migrations/ (ordered SQL), seed/, tests/firewall/ (proof harness)
scripts/ # Data import (xlsx → SQL) and seed generation
reference/ # Source workbooks, redesign prototype, dataset verification
middleware.ts # Auth/role gate vercel.json # cron config next.config.ts
sdk/* wraps all Supabase queries behind typed functions (submitCargo, getCurrentUser, …).lib/portal/adapters.ts maps raw DB rows to portal "view" models (CargoView, VesselView).DashboardProvider centralizes account + UI state for the portal.requireAdmin({ section, edit }) and middleware gates enforce access uniformly (tiered sub-admin permissions).lib/schemas/* validate forms and shape SDK payloads.The backend is Postgres on Supabase, defined entirely as code in supabase/migrations/ (~70 ordered files). The schema is migration-driven — there is no ORM; every table, enum, view, RLS policy, trigger, and RPC is plain SQL. Apply migrations in filename order, then seed with supabase/seed/unified_dataset.sql.
auth.users (Supabase Auth)
│ supabase_user_id
▼
organizations ◄──┐ public.users ──┬───► profiles (cargo | vessel)
│ │ │ │
organization_members ┘ │ └──► user_activity_log (GDPR)
(org_id, user_id) │
│ │ owner_user_id / owner_org_id
▼ ▼
┌───────────── listing_ownership ─────────────┐
│ (polymorphic: listing_type + listing_id) │
└──────┬───────────────────────────┬──────────┘
│ │
cargo_listings vessel_availability
│ │ │ │ │ │
│ │ │ └─ cargo_parcels │ └─ FK vessel_id ─► vessels
│ │ └───── cargo_safety_answers │ │
│ └───────── FK commodity_id ─► commodities └─ vessel_contacts
│ FK load/disch_port_locode ─► ports ◄── FK open_port_locode
│ │
│ └─ port_routes (port-pair distances)
└─ both cargo & vessel_availability ──► review_queue (moderation)
commodities ─► commodity_map · grain_list · imsbc_codes · css_categories (classification reference)
fuel_products · bunker_suppliers ─► bunker_supplier_ports · bunker_supplier_members · bunker_quotes ─► bunker_quote_events
market_insights_editions · market_insights_subscribers fuel_prices · voyage_estimates
contact_messages (public contact form)
| Table | Purpose | Key columns / relationships |
|---|---|---|
| users | App-level user (≠ auth.users) | id (PK), supabase_user_id → auth.uid(), role (user_role), access_tier, active |
| profiles | Cargo/vessel persona per account | account_id → users.id, profile_type (cargo|vessel), unique per (account_id, profile_type) |
| organizations | The company — carries subscription tier + desk contact channel | org_type, subscription_tier, desk_email/phone |
| organization_members | A person's seat in a company (M:N) | PK (org_id, user_id), member_role (admin|broker|viewer) |
| listing_ownership | Polymorphic, durable ownership of a listing | listing_type + listing_id, owner_user_id, owner_org_id, is_current, one primary per listing |
| cargo_listings | A cargo on the market | commodity_id → commodities, load/disch_port_locode → ports, qty_min/max_mt, laycan_*, review_status |
| cargo_parcels | Multi-port parcels of a cargo | FK → cargo_listings |
| cargo_safety_answers | Answers to DG/grain safety questions | FK → cargo_listings, question_id → safety_questions |
| vessels | Admin-managed vessel intelligence register (owners don't insert here) | imo_number (unique), vessel_type, dwt_grain/bale, certs, contact PII columns (firewalled) |
| vessel_availability | An open position posted against a vessel |
user_role · access_tier · access_granted_by · trust_tier_enum · zone_enum (B.SEA, E.MED, AG, R.SEA, R.SEA.N, R.SEA.S, BALTIC, GLAKES, F.EAST …) · port_type_enum · cargo_type_enum (Dry Bulk | Break Bulk) · imsbc_category_enum · answer_type_enum · vessel_type_enum (Bulk Carrier | General Cargo | Cargo Ship | Other — "Cargo Ship" is the workbook alias of "General Cargo") · flag_category_enum · scope_enum · risk_level_enum · vessel_status_enum (OPEN/FIXED/ON SUBS/INACTIVE/BALLAST/OFF-HIRE) · cargo_status_enum · cargo_priority_enum · review_status_enum (PENDING/APPROVED/REJECTED/FLAGGED) · load_terms_enum · ownership_role_enum (primary/co_broker/admin_posted) · transfer_reason_enum · profile_type_enum.
Enum values evolve as the CargoMap workbook does; the live enum set is authoritative — verify with the Supabase dashboard rather than assuming from this list.
| View | Role |
|---|---|
cargos_access_view | Cargo rows with contact NULLed unless admin or the listing's owner (20260601000100_…contact_firewall.sql) |
v_vessel_detail | Vessel specs visible to all members; contact PII masked to admin/owner (20260601000600_prod_vessel_contact_firewall.sql) |
v_my_vessels | The caller's own vessels + open positions |
v_account_profiles | Account + its cargo/vessel profiles |
v_admin_queue / v_admin_queue_detail | Moderation queue read models |
Privileged / firewall-safe logic lives in SECURITY DEFINER functions invoked by name. Notable ones (file → caller):
get_matches_for_cargo, get_matches_for_availability — authoritative match funnel (sdk/app/cargos.ts, sdk/app/vessels.ts).create_cargo_listing, create_vessel_availability, register_vessel, set_listing_circulation.resolve_cargo_classification, validate_cargo_classification (sdk/app/classification.ts).get_public_stats, get_public_platform_totals (app/(public)/page.tsx, components/PublicStatsBar.tsx).fn_publish_market_insights_edition (cron), get_latest_market_insights, get_market_insights_edition, get_market_insights_archive (lib/market-insights.ts, app/api/cron/market-insights/route.ts).fn_my_org_ids, fn_my_membership, fn_my_admin_org_id, fn_org_team, fn_org_manage_member, fn_search_organizations, fn_request_org_membership, fn_pending_membership_requests, fn_decide_org_membership (sdk/app/org.ts, ).⚠️ Reality check: the live DB has drifted from the code's expectations in places — some RPCs above were restored recently and at least one (
create_cargo_listing) is still missing on the live project, so portal cargo posting doesn't work until it's rebuilt. Always verify an RPC/table/column exists on the live schema before depending on it. Full status in ONBOARDING.md §5.
users.id ≠ auth.uid() — always resolve via supabase_user_id (see lib/app-user.ts). This is the single most common source of bugs.PENDING in review_queue; admins approve/amend/strike (goes_live_at controls publication).fn_set_updated_at maintains updated_at; fn_va_port_autofill derives port name/zone from LOCODE on insert/update.This project uses Supabase as an integrated backend. Storage and Realtime are not used.
| Supabase service | Used? | Where (exact files) |
|---|---|---|
| Postgres Database | ✅ Core | All supabase/migrations/; data access throughout sdk/ and lib/portal/data.ts |
| Row-Level Security (RLS) | ✅ Core | Policies in every migration; firewall in 20260601000100_…, 20260601000200_…; proof in supabase/tests/firewall/ |
Database Functions / RPCs (SECURITY DEFINER) | ✅ Core | Defined in migrations; called via .rpc(...) in sdk/app/, lib/market-insights.ts, app/api/, admin pages |
| Auth — Email/Password | ✅ | signInWithPassword (sdk/auth.ts, app/(dashboard)/dashboard/account/actions.ts) |
Prerequisites: Node.js 20+, npm, and a Supabase project (free tier is fine).
# 1. Install dependencies
npm install
# 2. Configure environment — copy the template and fill in Supabase keys
cp .env.local.example .env.local # then edit (see Environment variables below)
# 3. Set up the database (in the Supabase SQL editor):
# a) run every file in supabase/migrations/ IN FILENAME ORDER (oldest → newest)
# b) then run supabase/seed/unified_dataset.sql to load the real dataset
# 4. Run the dev server
npm run dev
Open http://localhost:3000.
/, /services, /market-insights, /contact/auth/signup → verify email (OTP) → /dashboard/admin/* (requires an admin-role account — see supabase/seed/promote_admin.sql)Tip: set the Supabase Site URL (Auth → URL Configuration) to
http://localhost:3000so verification / reset links resolve in local dev.
Useful scripts
npm run build # production build
npm start # serve the production build
npm run lint # eslint
See RUN_LOCALLY.md for the detailed walkthrough.
Supabase is the managed backend in every environment (there is no self-hosted DB step):
supabase/migrations/ into the SQL editor in order, or use the Supabase CLI:
supabase link --project-ref <your-ref>
supabase db push
supabase/seed/unified_dataset.sql (and promote_admin.sql to grant yourself admin)..env.local (local) and into Vercel (production).The app is a standard Next.js project — Vercel is the recommended host (it also runs the cron in vercel.json).
next build, output handled automatically).*.vercel.app URL.npm i -g vercel
vercel login
vercel link # link the local folder to a Vercel project
vercel env add NEXT_PUBLIC_SUPABASE_URL production # repeat per variable
vercel --prod # production deploy
vercel.json registers a weekly cron hitting /api/cron/market-insights (Mondays 06:00 UTC). Vercel Cron is available on its own plans; the route also accepts a manual Authorization: Bearer <CRON_SECRET> call so you can trigger it yourself. Set CRON_SECRET in Vercel to protect the endpoint.
SUPABASE_SERVICE_ROLE_KEY breaks signup/cron).Goal: serve the app on your custom domain (e.g. arabshipbroker.com) bought at Namecheap.
In Vercel: Project → Settings → Domains → Add. Enter your domain (add both arabshipbroker.com and www.arabshipbroker.com). Vercel shows the DNS records it needs.
In Namecheap: Dashboard → Domain List → Manage → Advanced DNS.
Add the records Vercel asks for. Typical setup:
| Type | Host | Value | Notes |
|---|---|---|---|
A | @ | 76.76.21.21 | apex → Vercel (use the IP Vercel shows) |
CNAME | www | cname.vercel-dns.com | subdomain → Vercel |
Remove Namecheap's default "parking"/URL-redirect records that conflict. Set TTL to Automatic. (Alternatively, point Namecheap's Custom DNS / nameservers at Vercel's nameservers if you prefer Vercel to manage DNS entirely — Vercel will list them.)
Wait for propagation (minutes to a few hours). Vercel auto-issues a Let's Encrypt SSL certificate once DNS validates.
In Vercel, choose the primary domain and let it redirect the other (e.g. www → apex or vice-versa).
Update the Supabase Site URL + redirect URLs to the final domain.
Yes — automatic deploys are straightforward. There are two clean approaches.
Vercel's GitHub app already gives you CI/CD with zero workflow code:
main → automatic Production deploy.Setup: connect the repo (see Deploying to Vercel → Option A). Optionally set Production Branch = main under Settings → Git. This is the least-maintenance option and what most Next.js teams use.
Use this if you want to block deploys on lint/build/tests or add custom steps. Add .github/workflows/deploy.yml:
name: Deploy to Vercel
on:
push:
branches: [main]
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run lint
- run: npm run build
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
deploy:
needs: ci
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Vercel CLI
run: npm i -g vercel
- name: Pull Vercel env
run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
- name: Build
run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
Add these GitHub repo secrets (Settings → Secrets and variables → Actions):
VERCEL_TOKEN (Vercel → Account → Tokens), VERCEL_ORG_ID, VERCEL_PROJECT_ID (from .vercel/project.json after vercel link), plus any build-time NEXT_PUBLIC_* vars.
If you use Approach 2, disable Vercel's automatic Git deploys for the production branch (Settings → Git → Ignored Build Step or disconnect auto-deploy) to avoid double-deploying.
| Variable | Scope | Required | Purpose |
|---|---|---|---|
NEXT_PUBLIC_SUPABASE_URL | client + server | ✅ | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY | client + server | ✅ | Anon key (RLS-scoped access) |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | client | ➖ | Newer publishable key (where used) |
SUPABASE_SERVICE_ROLE_KEY | server only | ✅ | Privileged ops: signup, account deletion, cron. Never expose to the client. |
ANTHROPIC_API_KEY | server | ➖ | Enables the AI circular/Q88 parser |
NEXT_PUBLIC_ASSISTANT_ENABLED | client | ➖ | Set true to surface the assistant/parser UI |
CRON_SECRET | server | ➖ | Bearer token guarding /api/cron/market-insights |
All secrets live only in .env.local (local) and in Vercel's Environment Variables (deployed) — .env* is git-ignored.
Testing & quality
lib/portal/matching.ts and econ.ts (pure functions, high value), plus Playwright E2E for the auth → post → match flow.supabase/tests/firewall/proof.sh) into CI so a regression in RLS/grants fails the build.Type safety & DX
supabase gen types typescript) to replace the Record<string, unknown> casts in the SDK and getAppUserRow.CLAUDE.md / CONTRIBUTING.md documenting the migration-order rule and the three-client trust model.Database & migrations
supabase db diff / db push) and a shadow database so migrations are validated in CI rather than pasted manually.Performance & resilience
/api/circulars/parse; extend to all route handlers.cache_control) and consider structured tool-use output instead of JSON-string extraction for the parser.Security
next.config.ts's hardcoded allowedDevOrigins IP out of source / behind an env check.next.config.ts headers or middleware.Product
alerts surface is a natural hook).Next.js 16 · React 19 · Tailwind v4 · Supabase · Anthropic — see RUN_LOCALLY.md for the local setup walkthrough.
vessel_id → vessels, open_port_locode → ports, open_date, status, review_status |
| vessel_contacts | Per-vessel contact records (PII) | FK → vessels |
| ports | Port reference (LOCODE keyed) | locode (PK), zone (zone_enum), latitude/longitude |
| port_routes | Port-pair routing/distance | references ports |
| commodities | Canonical commodity catalogue | imsbc_category, is_dg, is_grain, default_sf_m3t |
| commodity_map / grain_list / imsbc_codes / css_categories | Cargo-classification reference data | feed resolve_cargo_classification |
| safety_questions | Configurable DG/grain question bank | applies_to_cargo_type/categories, is_matchmaking_field |
| review_queue | Moderation funnel for cargo & vessel listings | listing_type + listing_id, status, action_taken |
| bunker_prices / bunker_suppliers / bunker_ingest_accounts | Bunker (fuel) price feed + ingest auth | for the bunker ticker & voyage costs |
| fuel_prices / voyage_estimates | Voyage-estimator inputs & saved runs | — |
| market_insights_editions | Immutable, dated weekly editions | written by the cron RPC |
| market_insights_subscribers | Newsletter subscribers | — |
| contact_messages | Public contact-form submissions | — |
| user_activity_log | GDPR activity log / erasure trail | FK → users |
| cargos (legacy) | Original cargo table, superseded by cargo_listings | retained for migration history |
get_admin_stats, get_admin_ops_stats, get_admin_activity, admin_set_bunker_credential.bunker_ingest, get_bunker_ticker, fn_position_checkin.commit_sync_batch (workbook → live tables, reversible, auto-approves marketplace listings), undo_batch, edit_live_record / bulk_update_live_records, resolve_commodity_review, resolve_vessel_review, sync_vessel_positions (02_VESSELS open rows → vessel_availability). See lib/sync/ + app/(admin)/admin/data-sync/.| Auth — Email OTP verification | ✅ | verifyOtp, resend (sdk/auth.ts, app/(auth)/auth/verify-email/page.tsx) |
| Auth — Password reset | ✅ | resetPasswordForEmail, updateUser (sdk/auth.ts) |
| Auth — Admin API (service role) | ✅ | auth.admin.createUser / deleteUser (sdk/auth.ts, app/(dashboard)/dashboard/account/actions.ts) |
| Auth — Session in SSR | ✅ | Cookie-bound session + middleware gate (middleware.ts, lib/supabase/server.ts, lib/admin/require-admin.ts) |
| Auth — client session/state | ✅ | getSession, onAuthStateChange (components/Navbar.tsx, components/footer-portal-link.tsx) |
| Supabase JS clients (3, by trust level) | ✅ | browser anon (lib/supabase/browser.ts), server cookie (lib/supabase/server.ts), service-role (lib/supabase/admin.ts) |
Supabase SSR helper (@supabase/ssr) | ✅ | middleware.ts, lib/supabase/server.ts, lib/admin/require-admin.ts |
| Storage | ❌ | Not used — uploaded Q88 PDFs are streamed to the Anthropic API in-memory, not persisted |
| Realtime / Channels | ❌ | Not used — data refreshes via re-fetch / ISR |
| Edge Functions | ❌ | Not used — server logic runs as Next.js route handlers / server actions on Vercel |