Ecommerce Template
Next.js·React 등으로 만든 최신 전자상거래 템플릿
Urtrag (Уртраг): 승객이 짐을 광고하고 배송원을 찾는 플랫폼
Urtrag (Уртраг) — аялагчид сул ачааны жингээ зарлаж, ачаа илгээгчид хүргэх хүнээ олдог платформ. Дэлхийн аль ч хоёр хотын хооронд, шууд мессежээр тохиролцоно.
커머스TypeScript · PLpgSQLGitHub @dashaa1204온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Peer-to-peer package delivery marketplace — Next.js 16, Supabase (Auth/Realtime/Storage/RLS), Drizzle ORM
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A platform that connects travellers flying between Austria and Mongolia with people who need something shipped.
Stack: Next.js 16 (App Router) · Tailwind 4 · Supabase (Postgres + Auth) · Drizzle ORM · Cloudinary (images)
Create a new project at supabase.com/dashboard.
Pick the eu-central (Frankfurt) region — it is the closest one to Austria.
Fill in .env.local with the values from your Supabase Dashboard:
This runs the migrations in drizzle/, creating the tables, the RLS policies and
the auth.users → profiles trigger.
Supabase Dashboard → Authentication → URL Configuration:README 전문 보기 ↗A platform that connects travellers flying between Austria and Mongolia with people who need something shipped.
Stack: Next.js 16 (App Router) · Tailwind 4 · Supabase (Postgres + Auth) · Drizzle ORM · Cloudinary (images)
Create a new project at supabase.com/dashboard.
Pick the eu-central (Frankfurt) region — it is the closest one to Austria.
cp .env.example .env.local
Fill in .env.local with the values from your Supabase Dashboard:
| Variable | Where to find it |
|---|---|
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY | Project Settings → API |
SUPABASE_SERVICE_ROLE_KEY | Project Settings → API (keep it secret!) |
DATABASE_URL | Settings → Database → Connection string → Transaction pooler (6543) |
DIRECT_URL | Same place, but port 5432 |
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET | cloudinary.com → Dashboard (the secret stays secret!) |
npm run db:migrate
This runs the migrations in drizzle/, creating the tables, the RLS policies and
the auth.users → profiles trigger.
Supabase Dashboard → Authentication → URL Configuration:
http://localhost:3000 (the real domain in production)http://localhost:3000/auth/callbackThat single route serves both signup confirmation and password recovery
(/auth/callback decides where to go next from the ?next= parameter).
Supabase's free SMTP sends only a handful of emails per hour, and only to addresses belonging to team members. Connect your own SMTP (Resend, Postmark, etc.) under Authentication → Emails before you work with real users.
Signing in with Google needs two extra pieces of setup:
https://<project-id>.supabase.co/auth/v1/callback
(copy it from Supabase → Authentication → Sign In / Providers → Google).The app itself needs no extra environment variables — the button only redirects
to /auth/callback, which is already in the Redirect URLs list from step 2.
A user created through Google arrives without a phone number. They can complete their profile at
/settings.
npm run setup:storage
This creates one bucket in Supabase:
identity-docs (private) — identity documents. Only the server side
(service role) can reach them, and moderators get a 5-minute signed URL. The
file is deleted as soon as a decision is made.Profile pictures live on Cloudinary (crowdshipping/avatars/<uuid>/<timestamp>)
— no setup required, the folder is created on the first upload. Images are
delivered through the f_auto,q_auto,c_fill,g_face,w_160,h_160 transformation,
so a 5 MB file reaches the browser as roughly 15 KB. The profiles.avatar_path
column stores the Cloudinary public_id; uploading a new picture deletes the
old one.
Moderator access is granted through ADMIN_USER_IDS in .env.local (user
uuids, comma-separated). Someone with access gets a "Moderation dashboard" link
in their profile menu; for everyone else the whole of /admin returns 404 — it
does not even reveal that the page exists.
| Page | What it does |
|---|---|
/admin | Overview: user, listing, deal and message counts, plus recent activity |
/admin/users | User list — search by name, see listings/deals/reviews |
/admin/listings | All listings — filter by type/status, close, reopen, delete |
/admin/deals | Deals by status. Only the message COUNT is shown, never the content |
/admin/verifications | Review identity document requests |
/admin/log | Moderator action history — who, when, what |
Because moderators act on other people's listings, every action is written to
the admin_actions table (closing/reopening/deleting a listing, approving or
rejecting a document). The log is append-only: there is no path in the app to
edit or delete an entry. The moderator's name is copied in as it was at the
time, with no FK into profiles — the record must outlive the person who
created it.
npm run db:seed
Creates the users [email protected] and [email protected] (password test12345)
along with sample listings and parcels.
npm run dev
src/
app/ # routing only — thin pages
views/ # the main UI for each page (+ components/)
components/ # shared layout / ui
lib/
db/ # Drizzle schema + Postgres connection
supabase/ # Supabase clients (server / admin / client — realtime)
cloudinary.ts # image upload / delete (server side)
avatar.ts # public_id → delivery URL
auth.ts # getCurrentUser / requireUser / requireAdmin
data.ts # every DB query
actions.ts # server actions
admin-data.ts # moderator-only queries (overview, lists)
admin-actions.ts # moderator-only actions (on other people's listings)
proxy.ts # refreshes the Supabase session (replaces middleware.ts in Next 16)
drizzle/ # SQL migrations
scripts/ # seed.ts (test data), setup-storage.ts (bucket)
| Command | What it does |
|---|---|
npm run db:generate | Generates a new migration from schema changes |
npm run db:migrate | Runs the pending migrations |
npm run db:studio | Drizzle Studio — browse the data from the browser |
npm run db:seed | Test data |
npm run setup:storage | Creates the private document bucket |
RLS is enabled on every table. The app connects directly through DATABASE_URL
and does its authorization checks in the server actions and in requireUser.
Policies exist only on conversations, messages and reviews, and only for
SELECT — the first two for the participants of a conversation, reviews for the
person who received the review. They are required so that realtime notifications
can be filtered per Supabase user (see the section below). The remaining tables
have no policies, so nothing can be read through the public REST API with the
anon key; INSERT / UPDATE / DELETE are equally closed on every table.
When a new message or review arrives, RealtimeSync (inside the navbar) listens
to the messages (INSERT / UPDATE) and reviews (INSERT) tables over Supabase
Realtime and calls router.refresh(). Nothing is assembled on the client — the
server re-renders, so the unread counter, the notifications, the inbox and any
open conversation all update at once, and the authorization checks stay on the
server.
The client does not pick which rows it receives — the
messages_select_participant and reviews_select_reviewee policies make sure an
unrelated user never gets the notification at all. On a fresh Supabase project,
migrations 0007 and 0008 add messages and reviews to the
supabase_realtime publication.
The notification bell shows the unread count from reviews.read_at — opening the
bell marks everything as seen. If a review is edited, read_at is cleared again
and the notification comes back.
Online status (PresenceProvider) and the "typing" indicator never touch the
database — they run purely over Realtime's presence / broadcast channels.