尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Branded quotations, invoices and receipts for freelancers and companies.
A business sends a quote → the customer opens a link, accepts and pays (Stripe, Paystack, Flutterwave, Bachs or bank transfer) → the quote becomes a paid invoice and a numbered receipt is issued. Every document carries the business's own logo, colour and details. Each quote can hold hidden cost lines (data, electricity, transport, software…) so the team sees real profit and margin; customers never see them.
Stack: Next.js 16 (App Router) · Supabase (Postgres, Auth, Storage) · Tailwind CSS 4 · pdf-lib. Design system: Kasuwa (Figtree, cool-grey canvas, one confident blue).
| Area | Where | Who |
|---|---|---|
| Sign up, onboarding (freelancer or company, branding, money, team) | /signup, /onboarding | Everyone |
| Dashboard: owed, collected, open quotes, profit, needs-attention | /app | Members |
| Quotes and invoices with hidden costs, margin and a pricing helper | /app/quotes, /app/invoices, /app/documents/[id] | Members (costs: allowed members only) |
| Customers, payments and receipts ledger | /app/customers, /app/payments | Members |
| Business and branding, payment setup guide, team and roles, plan and billing, cost presets | /app/settings/* | Owner / manager (billing: owner) |
| Customer's link: view, accept/decline, pay, download PDF | /d/[token] | Customers (no login) |
| Receipt | /r/[token] | Customers |
| Platform admin: plans and pricing, trial rules, businesses, subscription billing | /admin | You (platform owner) |
| Role | Can do |
|---|---|
| Owner | Everything, including billing. |
| Manager | Approves and sends, manages team (not other managers), branding and payments. |
| Finance officer | Creates, sends, records payments. Sees hidden costs. |
| Junior finance | Prepares drafts and submits them for approval. Sees hidden costs only if allowed. |
These rules are enforced in the database (row-level security and checked functions), not just in the UI.
supabase/migrations/ in order: first 20261024000100_paytrail.sql (every table, the security rules and the logos storage bucket), then 20261024000200_global_payments.sql (adds Stripe and Paystack), then 20261024000300_pricing.sql (default plans and a 14-day trial), then 20261024000400_privacy.sql (data download and deletion), 20261024000500_templates.sql (document templates and paper sizes) and 20261024000600_security_codes.sql (emailed security codes). All are safe to run again later.
https://paytrail.yourdomain.comhttps://paytrail.yourdomain.com/auth/callback (and http://localhost:3000/auth/callback for local work)smtp-relay.brevo.com, port 587, your Brevo SMTP login and SMTP key). The built-in sender is rate-limited.
supabase/email-templates/confirm-signup.html and set the subject to Your Usetally code: {{ .Token }}. New users type this 6-digit code to confirm their email (the link in the email works too).anon key and the service_role key for step 2.Copy .env.example to .env.local (locally) or add them in Vercel → Settings → Environment Variables:
| Variable | What it is |
|---|---|
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY | From Supabase. Keep the service role key secret. |
NEXT_PUBLIC_SITE_URL | Your app's public address, with no trailing slash. Used in payment links and webhook URLs. |
APP_ENCRYPTION_KEY | 32+ random characters. Encrypts every business's payment keys. Generate with node -e "console.log(require('crypto').randomBytes(32).toString('base64'))". Never change it later, or saved keys can't be read. |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, MAIL_FROM | Brevo: smtp-relay.brevo.com, 587, your Brevo SMTP login (e.g. [email protected]), your SMTP key (xsmtpsib-…), and a sender you verified in Brevo. Needed for security codes, emailing documents and admin alerts. Without it Usetally still runs, but security codes can't be sent, so that protection is off. |
ADMIN_ALERT_EMAILS | Optional. Who gets admin alerts (new businesses, payment key changes). Empty = every platform admin. |
CRON_SECRET | Random string. Protects the job that re-checks unconfirmed payments. |
LEGAL_COMPANY_NAME, LEGAL_ADDRESS, LEGAL_EMAIL | Shown on /terms, /privacy and /dpa. Use your registered business name and address. |
LEGAL_DATA_REGION, LEGAL_EMAIL_PROVIDER | Shown on /subprocessors, e.g. European Union (Frankfurt) and Brevo. |
PAYTRAIL_ALLOW_TEST_PAYMENTS | Leave unset in production. Set to 1 only on a staging site to try test-card payments end to end. |
vercel.json also runs /api/cron/reconcile once a day as a safety net (allowed on the free Hobby plan). On Vercel Pro you can make it every 15 minutes: change the schedule to */15 * * * *./terms and /privacy are ready-made starting points written for a Nigerian business serving customers worldwide. Have a lawyer review them before launch.Sign up in the app first, then in the Supabase SQL Editor run:
insert into public.platform_admins (user_id)
select id from auth.users where email = '[email protected]';
Refresh the app. Platform admin now appears in the sidebar (/admin).
Default plans (added by the pricing migration, all editable):
| Plan | Naira monthly / yearly | US dollar monthly / yearly | Documents a month | Users |
|---|---|---|---|---|
| Solo (freelancers) | ₦3,000 / ₦30,000 | $5 / $50 | 50 | 1 |
| Team | ₦7,500 / ₦75,000 | $15 / $150 | 200 | 5 |
| Business | ₦20,000 / ₦200,000 | $39 / $390 | Unlimited | 15 |
| Founding (hidden) | ₦60,000 a year, locked | — | 200 | 5 |
Businesses see plans in their own currency (dollars if there's no plan in theirs). There's no free-forever plan: after the 14-day trial a business keeps all its data but needs a paid plan to send new quotes and invoices. Assign Founding (Team features) to early customers from /admin/businesses → Manage; it isn't shown publicly and its price doesn't change when you raise public prices.
/admin/plans: edit or add plans: name, price, monthly or yearly, document limit, team seats, whether companies can choose it, and feature bullets. Prices show on the landing page and on every business's billing page immediately.
/admin/settings:
Built in:
/api/cron/reconcile) permanently removes it after 30 days, logo included. Usetally keeps only the record of plan payments made to it, with the business name, for tax./privacy, /terms, /dpa (Data Processing Agreement, covering businesses' customer data) and /subprocessors. Keep /subprocessors current (subprocessors() in lib/legal.js).Your side (not code): register with the Nigeria Data Protection Commission once you reach the threshold, file the yearly compliance audit return, name a Data Protection Officer if required, accept Supabase's and Vercel's DPAs in their dashboards, and keep a one-page breach plan (tell affected businesses within 48 hours, the regulator within 72). If you actively market to the EU or UK, appoint a representative there. Have a lawyer review the legal pages.
/print/..., /d/[token]/print, /r/[token]/print) and the PDF download follow the same template rules and paper sizes. Thermal rolls print as one page exactly as long as the receipt. In the print dialog choose the receipt printer, margins "None" and scale 100%.assets/fonts (PDF) and public/fonts (screen and print).ADMIN_ALERT_EMAILS (or every platform admin) when a business finishes sign-up, and when payment keys are connected, changed or removed. Keys themselves are never emailed./offline with links to the saved ones. Creating, sending and payments need a connection. Signing out clears saved pages from the device. The service worker (public/sw.js) only runs in production builds.Each business connects its own payment account in Settings → Payment setup, using the in-app guide. The screen suggests the best provider for the business's currency:
| Provider | Where it works | Webhook secret |
|---|---|---|
| Stripe | US, UK, EU, Canada, Australia and 40+ other countries | Signing secret (whsec_…) |
| Paystack | Nigeria, Ghana, Kenya, South Africa, Côte d'Ivoire | None (Paystack signs with the secret key) |
| Flutterwave | Across Africa | A secret hash you choose |
| Bachs | Where Bachs operates | Signing secret |
Setup steps:
A provider only shows Connected after the test payment is confirmed and its webhook has reached Usetally. Customers only see the Pay button for live-mode connections, so nobody can "pay" an invoice with a test card.
Every payment is re-verified with the provider's API before an invoice is marked paid. Amount, currency and reference must match exactly. A replayed webhook can't pay twice. If money arrives for a document that was already paid or cancelled, it's flagged as needs review in the document's activity.
Bank transfers, cash and POS are recorded by finance with Record payment, which issues a receipt the same way. Part payments are supported.
Stripe uses hosted Checkout Sessions and the
checkout.session.completedwebhook. Paystack uses Transaction Initialize/Verify and thex-paystack-signaturewebhook header.Bachs API note: the integration uses the Bachs endpoints from your existing CREATIVEYATI code (
/v1/checkout-sessions,/v1/balances, and HMAC-signed webhooks with thex-bachs-timestampandx-bachs-signatureheaders). Flutterwave uses the v3 Standard checkout andverif-hashwebhooks. Before going live, run one live test payment with each provider you enable.
npm install
cp .env.example .env.local # fill in a Supabase project (a free one is fine for development)
npm run dev # http://localhost:3000
Preview mode (no Supabase): sample data for every screen, local only. It is ignored in production builds.
PAYTRAIL_PREVIEW=1 PAYTRAIL_PREVIEW_ROLE=owner NEXT_PUBLIC_SITE_URL=http://localhost:3000 \
NEXT_PUBLIC_SUPABASE_URL=http://localhost:9 NEXT_PUBLIC_SUPABASE_ANON_KEY=x SUPABASE_SERVICE_ROLE_KEY=x \
APP_ENCRYPTION_KEY=preview-preview-preview-preview-xx npm run dev
Set PAYTRAIL_PREVIEW_ROLE to manager, finance or junior_finance to see what each role sees.
Tests
npm test # money maths, encryption, webhook signatures, provider responses
npm run test:db # 18 database security tests (needs a local PostgreSQL 16; see scripts/test-db.sh)
app/ pages and server actions (app/app = signed-in app, app/admin = platform owner, app/d + app/r = customer links)
components/ design-system components (ui.js), app shell, editor, document view
lib/ Supabase clients, permissions, money, PDFs, email, payments (stripe, paystack, flutterwave, bachs, webhooks)
supabase/ migration (schema + security) and database tests
assets/fonts/ Lato (open licence) for PDFs, which includes the ₦ sign
npm i --no-save playwright && npx playwright install chromium # once
npm run test:e2e # owner, manager, finance, junior finance
E2E_ROLES=junior_finance npm run test:e2e
Runs the app in preview mode with a simulated 100 ms delay on every Supabase call, then checks for each role: every allowed page opens without errors, refused pages redirect, the sidebar shows the right links, a click gets a visible response within 200 ms and the page is ready within 1.2 s, documents and the quote/invoice forms work, and the mobile menu works.