Open-Source Cold Email & Warmup Platform
Warmbly 是開源的冷郵件平台:溫暖您的收件箱、發送活動、並對每個回覆進行分流。可在我們的雲端運行或自行託管整個系統。
AI 翻譯
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗The internal side of adronis.app: one page that reads the same Supabase database the customer site uses, and changes the commercial part of an account — plan, trial end, next charge, discount.
Every trial and paid plan is a Paddle subscription. Paddle is where it is
charged, and the site's paddle Edge Function mirrors it onto the account.
So the portal never writes billing into the database as if a customer had
paid: a change to a paying account is made in Paddle, through that same Edge
Function, and the account follows from Paddle's answer.
It is a separate site. Nothing in the Adronis repository imports from here, and nothing here is served from adronis.app.
Can
errors.js), copy one out to fix it, and clear it once the fix is liveCannot
Open any account and the first thing in the drawer is one sentence saying where that account stands, then five buttons for where it should stand. Each one asks only for what it needs, and a line underneath spells out what pressing Apply will leave behind before you press it. Anything that charges the card or takes the plan away asks first.
The drawer says Pays through Paddle with the subscription and customer IDs,
and Apply goes to the Edge Function (admin_set_plan):
| What happens in Paddle | |
|---|---|
| Trial | Only while Paddle still has it on trial: the trial end moves to the new date. A paying account can't go back on a trial. |
| Paying | On a trial: the trial ends and the card is charged today. Already paying: the next charge moves to the date you pick, and the days in between are free. |
| Beta | Refused: a beta tester never pays. Set No plan first, then Beta. |
| Free forever | The Paddle subscription is cancelled today, then the account is given the plan for good (below). |
| No plan | Paddle cancels at the end of the period, or today. |
A plan or cycle switch is billed from the next charge, the same as when the customer switches on their own account page. If the customer already has a switch waiting, the editor opens on that plan, so Apply doesn't undo it.
Only Beta, Free forever and No plan do anything; all three go
through admin_set_plan_state, which refuses trials, paid plans, and any
account Paddle is billing. Trial and Paying say to send the customer to
checkout.
Beta is for the businesses picked for the beta, until the public launch in
Q1 2027. It is a plan of its own (plan = 'beta') with no price: it isn't in
the pricing section or at checkout, Paddle has no price for it, and this button
is the only way an account gets it. Underneath it is the same as free forever —
active, no renewal date, never charged, comped, with Beta tester as the
reason unless you write another — so the site lets the account in and nothing
ever renews or ends it.
It includes what the beta plan promises, and the customer's billing page lists it: 4 ads every Monday in two versions each, every image checked before the owner sees it, the approved ads posted for them, holiday drops, reels from approved ads once they are built, up to 4 channels, and 30% off a monthly plan for the first 12 months after the beta (the FOUNDER30 code in Paddle).
Beta testers have their own tile on the overview and are left out of Free forever. At launch, set each one to No plan and send them to checkout with the founder code.
It needs the beta value the site's supabase/schema.sql adds to plan_tier,
so run that file before this one.
An account that still has a dated plan set by hand from before Paddle is shown as such: nothing charges it, and the site ends it on its date.
Save changes sends it to the Edge Function (admin_set_discount). On a
paying account it goes onto the Paddle subscription at once, as a percentage off
every charge from the next one on — in place of any promo code used at checkout.
On any other account it is recorded, and the Edge Function puts it on the
subscription the moment the customer checks out, unless they used a promo code
there. Each percentage is one custom Paddle discount (hidden from the catalog,
can't be typed in at checkout), shared by every account on that percentage.
Every change the Edge Function makes writes the same admin_audit row the SQL
functions do, marked in Paddle in the log.
Deploy the site's supabase/functions/paddle again whenever it changes — the
portal's Paddle choices only work against a version that has these actions.
subscription_status = 'active' and current_period_end null.
The site already handles that pair without knowing anything about this portal.
hasActivePlan() is true, so the customer has the full plan. And
finalize_billing_period() returns the moment it sees a null period end, so
nothing ever rolls the period over, appends an invoice, or acts on a
cancellation. It simply stays.
The comped column records that the missing date was a decision rather than a
gap in the data. That is what keeps the account out of the monthly revenue
figure — it is active like any other, but it was given away — and what prints
free forever in the table instead of a missing date. comped_reason is the
one line saying why.
One change was needed on the customer site for this, in
adronis/account.js: with no renewal date there is no next invoice, so the
billing page no longer prints an upcoming charge, the status reads
Active — nothing to pay, and the switch-plan and cancel controls step out of
the way. Without it a comped account would have seen an invalid date and a
charge that is never coming.
Open the Supabase dashboard for the Adronis project, go to SQL Editor, paste
the whole of supabase/portal-admin.sql and run it. It is idempotent — running
it twice is harmless. Run it again whenever that file changes: the plan editor
calls a function that only exists once it has been run.
The last block of that file is the admin list. It already names
[email protected] and [email protected]; a third person
is one more line. Each address must already exist as a Supabase user — the
portal reuses the Adronis login, it does not create accounts of its own.
The file ends by printing the admin table, so the result pane tells you who
actually got in. An address that is missing from that list does not exist in
auth.users under that exact spelling.
You do not have to sign up as a customer to get in. In the Supabase dashboard, Authentication → Users → Add user: email, password, and tick Auto Confirm User. That is enough to sign into the portal.
Adronis creates a profiles row for every new user, so that account will have
one too — but it stays empty: no business name, no brief, no plan, no trial.
Nothing on the customer site treats it as a customer, and the portal hides
accounts that are in portal_admins from the account list and leaves them out
of every number on the overview, so your own login never shows up as a
phantom signup.
If you would rather use an account you already have on adronis.app, that works too — just add its address to the seed block.
To add or remove an admin later:
insert into public.portal_admins (user_id, email, label)
select id, email, 'partner' from auth.users where email = '[email protected]';
delete from public.portal_admins where email = '[email protected]';
Push this folder to its own Git repository and import it into Vercel as a new project — not as part of the Adronis project. No build command, no framework preset; it is static files.
Give it a URL nobody would guess or a subdomain like portal.adronis.app.
Anyone can open the page, but without an admin account they see the login
screen and nothing else: the database itself refuses to hand out a single row
to a signed-in user who is not in portal_admins.
Your normal Adronis email and password.
Half an hour with nobody touching the page ends the session: the portal signs out and the login screen says why. The last minute of that is spent saying so, with a bar at the bottom of the screen and a Stay signed in button — though any click, keypress or scroll anywhere is enough, so it is only there to stop a half-filled form disappearing under you.
Activity is shared between tabs, so working in one keeps the others alive. The check compares timestamps rather than trusting a timer, which means a laptop that slept through the afternoon comes back to an expired session rather than to a live one waiting for a timer that never fired.
This is not a security boundary by itself — it runs in the browser, and it is the database that decides what any token may read. It is for the ordinary case: the portal left open on a laptop, with every customer's email address and every figure in the business on the screen behind whoever walks past it.
supabase-config.js holds the same public anon key the customer site holds.
That key grants nothing by itself — every table has row level security on, and
what an admin may read is decided by policies that call is_portal_admin().
Writes do not go through table policies at all. In the database they go
through admin_set_plan_state, admin_update_account and admin_set_setting,
which each check is_portal_admin(), touch only the fields they name, and
write a row into admin_audit in the same transaction. Everything that touches
Paddle goes through the site's paddle Edge Function, which checks the caller
is in portal_admins before it does anything and writes the same audit row.
So there is no change made from this portal that does not leave a trace, and no
path from this portal to a customer's brief.
The service_role key is never used here and must never be pasted into this
folder. It bypasses row level security completely.
Earlier versions of supabase/portal-admin.sql replaced the site's
public.start_trial and granted it to every signed-in user, which let anyone
start a trial — or a paid plan — from the browser console without Paddle. The
current file never touches that function except to shut it, so it is safe to
run in any order with the Adronis supabase/schema.sql. Run the current one at
least once to close the gap if an older one was ever run after the schema.
| File | What it is |
|---|---|
index.html | The whole page: login gate, four tabs, edit drawer |
portal.js | Auth, data loading, rendering, every write |
portal.css | Adronis palette, laid out for dense reading |
supabase-config.js | Project URL and public anon key — same as the site |
supabase/portal-admin.sql | Admin list, policies, write functions, stats |
vercel.json | noindex headers, no HTML caching |