Sign in to a panel and trim shop portal with customer pricing tiers
AI summary · website and README
CommerceJavaScript · HTMLGitHub @direp972OnlineOwner 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
direp972/Fortified-Shop-App
Translation pending
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Fortified Sheet Metal — Shop Order Portal
Standalone version of the app with real login accounts and staff-assigned customer
pricing tiers — independent of Claude.ai's artifact environment.
What changed from the Claude artifact versionRead the full README ↗Standalone version of the app with real login accounts and staff-assigned customer pricing tiers — independent of Claude.ai's artifact environment.
Same app, same tools, same everything — but now:
supabase-setup.sql (included in this folder), copy everything above the
"ONE-TIME MANUAL STEP" section, paste it in, and click Run..env.example to a new file named .env.npm install
npm run dev
Open the URL it gives you (usually http://localhost:5173).
supabase-setup.sql with your email filled in:
insert into staff (id)
select id from auth.users where email = '[email protected]';
Repeat step 7 (with a coworker's email, after they sign up) for anyone else on your team who needs staff access.
Option A — Vercel (recommended, free tier):
ANTHROPIC_API_KEY there too. It powers Scan a Sketch in the trim
drawing tool (the api/scan-sketch.js function reads it server-side — the key never
reaches the browser). Without it, the button explains that scanning isn't set up yet.Option B — Netlify:
npm run build — creates a dist folder.dist folder onto Netlify's deploy page.The shop is emailed about every new order (order-alert) and every Get Listed
application (listing-alert) through Resend. Both Edge Functions
(source under supabase/functions/) read their settings from Supabase Vault, so nothing
has to be typed into the dashboard's secrets page. Open SQL Editor and run, with
your Resend API key in place of the placeholder:
select vault.create_secret('re_your_key_here', 'RESEND_API_KEY');
Optional settings are stored the same way: ALERT_EMAIL_TO (default
[email protected]), LISTING_ALERT_TO (default [email protected]) and
ALERT_EMAIL_FROM (default [email protected], which Resend only delivers to the
address on your own Resend account — verify your domain in Resend, then set this to
something like Fortified Metals Orders <[email protected]>).
To rotate a key, update the existing secret rather than creating a second one with the same name:
select vault.update_secret((select id from vault.secrets where name = 'RESEND_API_KEY'), 're_new_key_here');
Password reset goes through the same function: Forgot password? on either sign-in
form posts {email, reset:true}, and the email links to public/reset.html, where the
person types a new password and lands back signed in. A pending (never confirmed) account
gets a fresh confirmation link to the same page instead, so a forgotten first password is
no longer a dead end. Every address gets the same answer, so the form can't be used to find
out who is registered.
Sign-up confirmation emails go through the same key. Supabase Auth's own mailer is
capped at two emails an hour, so the signup-email Edge Function creates each account
unconfirmed, asks Supabase Auth for its confirmation code, and emails a link itself
through Resend from [email protected] (change the sender with a Vault secret named
SIGNUP_EMAIL_FROM). The link opens public/confirm.html on shop.roofcoil.com, where one
button press exchanges the code for a session and sends the person back to the page they
started from, signed in. The page exists because mail scanners such as Outlook Safe Links
open links before the person does and would burn a one-time code. Both sign-up forms (the site modal in public/auth.js and
the drawing app) use it, and a sign-in that fails with "not confirmed" offers a
Send a new link button. To keep bots from burning through the Resend quota, every
request takes a slot in public.signup_requests before anything else happens, and the
function allows 3 requests an hour for one inbox, 5 an hour from one connection, 30 an
hour and 80 a day overall (LIMITS at the top of the function). A pending account keeps
its password and details until its owner confirms, so asking for a link can never take one
over. When the function is redeployed, ship public/confirm.html first: every email links
to it. A CAPTCHA in front of the forms is the next step if bots ever become a problem.
Any directory listing can take orders from the Panel & Trim app:
/manage-listing.html; you apply the
edit like any other). Link the shop's account under Owner account so they get a
Shop Floor.?shop=<listing id> on the app URL
preselects a shop, which is how a shop can link to the tools from its own site later.orders.shop_id. The order-alert function emails that shop's order
email (the Fortified desk when there is no shop or no order email), and the customer
gets a confirmation with the shop's name and phone. The listing's owner account sees only
the orders sent to their shop on Shop Floor and can move them through the statuses;
the Master Materials List stays staff-only.Fortified Sheet Metal's own listing has ordering switched on with no order email, so its
orders keep going to ALERT_EMAIL_TO and the shop text exactly as before.
Every listing that takes orders is a shop on the platform (public.shops, same id as
the listing — a database trigger creates it the moment ordering is switched on), and the
listing's owner account is the shop's first member (public.shop_members, roles
owner and staff). A shop's people get, for their shop only: Price List → Backend (Edit)
with costs and margins, the coil width scale, saved trim presets, the Master Materials
List, the Customer Pricing Tiers panel and Shop Floor. Everything a shop saves lives in
public.shop_settings (one row per shop per key) and its tiers in public.shop_customers
(one row per shop per customer; no row means Tier 2), all isolated by RLS — a shop can
never read another shop's prices, costs, presets, board, members or customers. Platform
staff (public.staff) keep every power across every shop. Design and the reasoning:
docs/phase1-tenant-design.md; the two-shop checklist: docs/phase1-two-shop-test-plan.md.
?shop=<listing id> picks it (also a shop whose
listing is hidden, through ordering_shop()), otherwise a member's own shop, otherwise
the platform's default shop (Fortified). The picker under "Send this order to" changes
it; the price list, coil scale, presets, the account's tier and the backend all follow.submit-order reprices against the order's shop: that shop's price list and coil
scale (the code defaults until the shop has saved its own) at the tier that shop gave
the account. The first order an account sends a shop puts it on that shop's tier panel
at Tier 2; a shop can also add an account by email before its first order.app_storage keys (shop-price-list, …) were copied into
shop_settings by migration 20261017213414_shops_tenant.sql; the cleanup migration
20261017220436_phase1_cleanup_legacy_keys.sql then deleted them, returned app_storage to plain
signed-in read/write (it holds only the idea box now), and dropped customers.tier.docs/phase2-self-serve-shop-design.md, migration
20261020181706_phase2_self_serve_shops.sql). A listing becomes a shop the moment its owner
is linked (ordering off); Shop Floor → Shop Settings walks the owner through prices,
order email and phone, people, tier names and the ordering switch, which needs an order
email and a saved price list (the default shop is exempt). Owners invite people by email
(shop_invites; accepted by whichever account confirms that email), the listing's owner
account can never be removed, ?shop=<slug> works next to ?shop=<id>, tier names live
per shop under shop_settings key tier-labels, and PO numbers come from each shop's
own counter, assigned by submit-order. Deploy order: the migration (additive), then
submit-order, listing-alert and order-alert, then the build; the cleanup migration
20261020211937_phase2_cleanup.sql (applied 2026-10-20) then dropped next_po_number(),
po_number_seq and .Two steps and a sheet. What do you need picks the tool (panel, trim, a kit, metal, 3D parts); Order details is the tool itself. Finish & color is not a step you travel to: it opens as a sheet over whatever screen is up, and Done drops you back on it with the 3D model, the drawing and every typed number exactly as they were. A sticky bar at the top of the tab shows the three stops with what each holds (the part, the tool, the color) and each is a button, so the color is one tap away from the 3D tool and the chooser one tap away from a drawing.
The browser's Back button and a phone's back gesture close the sheet, close a kit, return
from the 3D tool to the Commercial Kit it was opened from, or step from a tool back to the
chooser, instead of leaving the app. Only moves the person made by hand leave a history
entry; deep links, the Job Vault and a kit opened after a send do not, and nothing is
pushed when members.html embeds the app. ?view=color (the Color Lab button) opens the
sheet over the chooser.
customer_price_list(shop), which drops the cost column. The
tier prices stay in that payload because the app computes every estimate in the browser.supabase/functions/_shared/pricing.mjs, shared by the app and the submit-order Edge
Function: the app shows the estimate as the person types, and on Send the function
prices the order again at that account's tier against the shop's live Price List, writes
the row itself and replies with the real price. Once the build that calls it is live,
apply supabase/migrations/20261017200506_orders_server_priced.sql, which drops the
browser's insert right on orders, so a tampered page can't send a price of its own.
node scripts/pricing-check.mjs checks the tier math against the seed price list.customers row from a database trigger the moment it
is confirmed, so it shows up in the tiers panel whether or not it has opened the app.customers.zip, shows next to the email in Customer Pricing Tiers, and a new account
that didn't arrive by a shop's link starts on the ordering shop nearest that ZIP.ordering_shop(uuid)directory_public view, which lists live rows without
owner_id, application_id or order_email. The anon role's grant on the table itself
is limited to those same columns, so a hand-written request can't read them either.
Signed-in admin pages read the table.public/places.js. The browser key goes in
public/site-config.js; restrict it in Google Cloud by HTTP referrer (roofcoil.com,
www.roofcoil.com, shop.roofcoil.com, fortifiedmetals.com, www.fortifiedmetals.com,
localhost) and by API (Maps JavaScript API, Places API (New)). A picked address carries
its coordinates, so the OpenStreetMap check at submit is skipped for it; with the key
empty every field behaves as before. One autocomplete session is billed per address
picked, not per keystroke.claim_listing() links the account
immediately when its confirmed email is at the listing's website domain and nobody owns
the listing yet; every other claim waits under Listing claims in Directory admin,
where decide_listing_claim() approves or dismisses it. The desk is emailed about a
pending claim and the claimant about the outcome (listing-alert, kinds claim and
claim_decided). Get Listed warns when the company or website is already on the
directory, and an application card in Directory admin flags the existing listing so the
changes go onto it instead of a second copy..github/workflows/build.yml). Make it a required check on main so a red build can't
be merged. The workflow reads VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY from the
repository's Actions variables (Settings > Secrets and variables > Actions > Variables)
— the same two public values as .env. The prerender has no built-in fallback for them,
so a build without them fails on purpose rather than pointing at the wrong project.