Lowballer.org
Screenshot any Facebook Marketplace listing or Copart lot. Lowballer pulls live comps, prices the repairs, and hands you the exact lowball offer or max bid to make.
B2B შეკვეთების პლატფორმა რესტორნებისთვის, კაფეებისთვის, სასტუმროებისა და მათი მომწოდებლებისთვის.
Translation pending
CommerceTypeScript · CSSGitHub @gadzadzeOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗B2B ordering for HoReCa businesses in Georgia: suppliers put their catalog and existing customers on Gusto, and those customers reorder in a few taps.
CLAUDE.mddocs/milestones/Status: milestones 1–6 (code complete for the pilot; production deployment not yet performed, see "Implemented, configured, verified").
RESEND_API_KEY, emails (including invitations) are not sent: they
are logged as [DEV EMAIL] and the UI says so.Next.js 16 (App Router, Cache Components) · TypeScript · Tailwind CSS v4 · Supabase (Auth, Postgres) · Drizzle ORM · next-intl (Georgian) · Vitest · Playwright. Hosted on Vercel + Supabase, both in Frankfurt.
npx supabase … works without a global install)npm install
cp .env.example .env.local # one file for the app, scripts and tests
npx supabase start # local Postgres + Auth + Studio + email catcher
supabase start prints the local API URL and keys. Put them in .env.local.
Newer CLI versions print sb_publishable_ / sb_secret_ keys; don't use those:
the Auth admin API (used to create allowed users) refuses sb_secret_ with
"invalid JWT". Run npx supabase status -o env and use the eyJ... keys:
.env.local | From supabase start |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | API URL (http://127.0.0.1:54321) |
NEXT_PUBLIC_SUPABASE_ANON_KEY | ANON_KEY from npx supabase status -o env (eyJ...) |
SUPABASE_SERVICE_ROLE_KEY | SERVICE_ROLE_KEY from npx supabase status -o env (the long eyJ... JWT) |
DATABASE_URL, DATABASE_URL_DIRECT | DB URL (postgresql://postgres:[email protected]:54322/postgres) |
Or let a script do it: npm run env:local reads npx supabase status -o env and
writes the right values into .env.local (keeping the rest, saving the old file
as .env.local.bak). It refuses anything that is not a local Supabase.
Then:
npm run db:migrate # apply Drizzle migrations to the local database
npm run dev # http://localhost:3000
Magic-link emails are caught by the local mail viewer (Mailpit/Inbucket), at the URL
supabase start prints. Google sign-in is off locally unless you configure it
in supabase/config.toml.
supabase/config.toml was written by hand with only the settings Gusto needs
(ports, redirect URLs, email templates). If your CLI version rejects a key,
run npx supabase init in a scratch folder and copy those settings over.
| Command | What it does |
|---|---|
npm run dev | Dev server |
npm run build / npm start | Production build / serve it |
npm run lint | ESLint |
npm run typecheck | Generate route types, then tsc --noEmit |
npm test | Unit tests (no database) |
npm run test:integration | Integration tests against DATABASE_URL_TEST (wiped on every run) |
npm run test:e2e | Playwright against a production build + real Supabase |
npm run db:generate | Generate a migration from src/server/db/schema.ts |
npm run db:migrate | Apply migrations (uses DATABASE_URL_DIRECT) |
npm run admin:grant -- <email> | Make an address a platform admin (also allows it to sign in) |
npm run user:allow -- <email> | Let an address sign in (invite-only app); optional --note "text", --role supplier|buyer, and --name "Business" --legal-id 123456789 (= you verified this business: it starts active) |
npm run env:local | Write the local Supabase URL and keys into .env.local (after supabase start) |
npm run db:seed | Fill a local database with [DEMO] data (refuses anything else) |
npm run check:secrets | After npm run build: fail if a server secret is in the client bundle |
src/server/db/schema.ts. Migrations are SQL files in
drizzle/, committed.schema.ts → npm run db:generate → read the
generated SQL → commit → npm run db:migrate.npx drizzle-kit generate --custom --name <what>.drizzle-kit push against a shared or production database.db:migrate targets whatever DATABASE_URL_DIRECT points at. Check it
before running.Unit (npm test): pure logic, no database.
Integration (npm run test:integration): domain logic against a real
Postgres.
The database must be disposable. The setup drops and recreates it, then applies the real migrations, so every run also proves the migrations work on an empty database.
It refuses to run unless the host is local and the database name contains
test.
Plain Postgres lacks Supabase's auth schema, so setup creates a minimal
stand-in: auth.users and the anon/authenticated roles.
Any local Postgres works:
createdb gusto_test
DATABASE_URL_TEST=postgresql://postgres:[email protected]:5432/gusto_test npm run test:integration
E2E (npm run test:e2e): drives the production build in Chromium against
the Supabase in .env.local (local: supabase start + db:migrate).
/auth/confirm route.SUPABASE_SERVICE_ROLE_KEY.PLAYWRIGHT_CHROMIUM_EXECUTABLE to a Chromium binary.Clean-database check. The app must work with zero rows. The e2e suite is
meant to run against a database that has only had db:migrate applied (no
seed): every spec creates its own admin, suppliers and buyers through the UI,
so a green run proves the empty-database path (including empty dashboards).
The persistence spec starts and stops its own web server on port 3101 to prove
orders survive a restart.
CI (.github/workflows/ci.yml) runs lint, typecheck, unit tests and build,
plus the integration tests against a Postgres service container. E2E is not in
CI yet: it needs a running Supabase.
Do this for each Supabase project (local is pre-configured in
supabase/config.toml).
https://gusto.ge.https://gusto.ge/**, plus http://localhost:3000/**
for local development against this project.Set Magic link and Confirm signup to link to:
{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email
supabase/templates/magic-link.html has the Georgian template.
The default templates use a different link format, and sign-in will fail with them.
smtp.resend.com, port 465;resend, password = a Resend API key;https://<project-ref>.supabase.co/auth/v1/callback)./auth/callback, which the redirect URL
pattern above allows.npm run db:migrate creates the public product-images bucket (1 MB
per object, WebP only).SUPABASE_SERVICE_ROLE_KEY, can write.Order events (submitted, confirmed, rejected, cancelled, delivered) write in-app notifications and email outbox rows in the same transaction as the order change. Delivery happens afterwards, so a mail problem never affects an order.
Email provider: Resend's HTTP API.
RESEND_API_KEY and EMAIL_FROM (a sender on a domain verified in
Resend) enable real sending. This is separate from the SMTP setting
above, which only covers Supabase Auth's own emails.[DEV EMAIL]
(subject only in production) and the delivery row is marked skipped.Delivery:
after() attempts that order's
pending deliveries./api/cron/notifications (POST or GET, header
Authorization: Bearer $CRON_SECRET) drains everything due: it claims
rows with FOR UPDATE SKIP LOCKED in batches of 50, retries after 1, 5,
15 and 60 minutes, then marks the row failed. Failures store a short
last_error, never a message body or token.Scheduling (Supabase Cron): Vercel Hobby cron is too infrequent, so
migration 0010_notifications_cron.sql schedules a job named
gusto-notifications every minute with pg_cron + pg_net. It skips
itself on plain Postgres. The URL and secret are not in the repo; the
job reads two Vault secrets at run time. Create them once per project
(SQL editor), before or after migrating:
select vault.create_secret('https://<your-app-domain>/api/cron/notifications', 'gusto_cron_url');
select vault.create_secret('<same value as CRON_SECRET>', 'gusto_cron_secret');
vault.update_secret(...).select * from cron.job_run_details order by start_time desc limit 10;
and responses with select * from net._http_response order by created desc limit 10;.net._http_response); in-app notifications and after() delivery
still work.Invitation emails are sent directly when the invitation is created, not through the outbox: the text contains the raw invite link, which Gusto never stores.
WhatsApp is user-sent only. A future Business API channel is sketched in .
Nobody can register themselves, which keeps spam out. An email address can request a sign-in link only if:
/admin/access, or
npm run user:allow -- [email protected] --note "Winiveria, owner" --role supplier); orRevoking an address (admin page) blocks it, even if an invitation would otherwise let it in. Everything else gets "this address isn't registered" and no email, and no account is created.
/suppliers (what Gusto is, plus a short form with business name,
legal ID, contact, phone, email, city). That creates no account, only a
request. You see it under Admin → Requests (email alert and sidebar count),
check the ID, call them, then Approve: their email goes on the allow-list as
a vetted supplier and they sign in like any supplier you registered yourself.
Reject needs a reason. Spam is limited by a per-IP limit and a hidden trap field./admin/suppliers. When you add an address you can pin its role
(--role supplier|buyer, or the Role field on the Access page): the person then
sees only that onboarding form, and the server refuses the other kind. Leave it
open and they choose./auth/confirm, /auth/callback) check
again and sign the person out if they are not allowed (this covers Google); and
Supabase itself has sign-ups switched off, so it only has the users the app
or its scripts created. Local Supabase gets this from supabase/config.toml.
On a hosted project, switch it off yourself: Authentication → Sign In / Providers →
turn off Allow new users to sign up.There are no passwords. A business's "credential" is its email address: it requests a link on the sign-in page and opens it.
Suppliers can't add products until at least one category exists:
/admin/access./admin/categories./admin/suppliers.npm run db:seed creates clearly fictional data, every name prefixed [DEMO]:
a Tbilisi restaurant, a café, a beverage distributor and a food distributor,
30 products, one custom price list, and seven orders in different statuses
(with their notifications).
DATABASE_URL points at a local host (localhost,
127.0.0.1, ::1) or at a host listed in SEED_ALLOWED_HOSTS (for a
dedicated dev Supabase project). It also refuses in production and when
[DEMO] organizations already exist.npm run admin:grant -- [email protected].There is no admin sign-up.
Someone with database access runs (this also lets the address sign in, so the person does not need to sign in first):
npm run admin:grant -- [email protected]
This uses DATABASE_URL_DIRECT from .env.local (or .env), and prints
the database host so you can confirm you targeted the right one.
The grant is recorded in audit_logs.
rate_limits table): order submit 10/min
per organization; invitation creation 30/hour per supplier; invitation
acceptance 10/min per client IP (stored hashed); image upload 30/hour per
supplier. A refused attempt shows "too many attempts, try again shortly".next.config.ts): a Content-Security-Policy that allows this
app, the Supabase project (for product photos) and self-hosted fonts;
X-Frame-Options: DENY; Referrer-Policy; X-Content-Type-Options;
Permissions-Policy; HSTS. Scripts and styles need 'unsafe-inline' because
Next.js emits inline ones and nonces would make every page dynamic.npm run check:secrets scans .next/static for the service-role
key's name and value, other server secret values, and service-role JWTs.npm audit --omit=dev reports 0 vulnerabilities. npm audit
also lists development-only findings (the ESLint config chain and
drizzle-kit's bundled esbuild). They never ship to production, and npm's
suggested "fix" is to downgrade eslint-config-next to 14.x and
drizzle-kit to 0.18, which would break this project, so they are left
until upstream releases fix them.What this repository does, what you must set up, and what has actually been exercised. Read the last column before relying on a row.
Software is not the only thing between a pilot and real users. Do these first; none is done in this repository.
[TEST], [E2E] or [DEMO] rows (the seed
never runs there), and only the people who should be admins are admins
(select email from auth.users u join profiles p on p.id = u.id where p.is_platform_admin;).eu-central-1 (Frankfurt);DATABASE_URL_DIRECT pointing at the project's
direct or session connection, then npm run db:migrate;fra1;.env.example, with DATABASE_URL = the
transaction pooler URL (port 6543);main via Vercel's Git integration.NEXT_PUBLIC_* values are inlined at build time. Redeploy after changing
them.The exact checklist is in docs/deploy.md.
docs/whatsapp-business-api.md0014npm run admin:grant -- [email protected] now also puts the
address on the list and creates its auth user, so you do not have to sign in
first. It needs NEXT_PUBLIC_SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY in
.env.local (the project it should act on).docs/deploy.md| Everything in it |
| Not performed. No Supabase, Vercel or Resend account was available |