HiveShop โ User Guide
Everything you need to buy and sell on HiveShop: signing in, posting an item with photos and variants, handling buyer inquiries, and (for administrators) running the console.
๐ What HiveShop is
HiveShop is a Philippine classifieds marketplace. Anyone can browse listings without an account; you only sign in when you want to post an item, save a favorite, or message a seller from your account.
Search, filter by category, price, condition and city, and open any listing โ no account needed.
Photos are resized on your phone before upload, so listing works even on a slow connection.
Paste a description and AI fills the title, price, condition, category and specs for you.
Buyers reach you by call, SMS, WhatsApp or an on-site inquiry. HiveShop does not handle payment.
๐ Signing in
Open login.html (the Login button in the header, or Me in the bottom bar on a phone). There are four ways in โ all land on the same account if they share the same mobile number or email address.
| Method | How it works | When to use it |
|---|---|---|
| SMS OTP | Enter your PH mobile (09XXXXXXXXX); a 6-digit code arrives by text and is valid for 10 minutes. | The default โ fastest on a phone. |
| Email OTP | Enter your email; the same kind of 6-digit code arrives by email. | No load, or you prefer email. |
| Password | Email or mobile plus a password you set in your profile. | Fastest once set up; needed on a desktop without your phone. |
| The Google button below the tabs โ one tap, no code. | If your Google account uses the same email. |
๐ท๏ธ Posting an item
Tap Sell (the pink circle in the middle of the bottom bar on a phone, or the Sell button in the header on a desktop). On a phone the form is a stepper โ one step at a time; on a desktop every section is on one page with a section rail on the left.
- Photos โ drag files in, tap to browse, or use Take a photo. The first photo is the cover.
- Details โ title, category, brand, condition, price and quantity. Optional Smart fill box at the top can do most of this for you.
- Specifications โ the fields shown here depend on the category you picked (storage for a phone, size for shoes, and so on).
- Variants โ optional. Use it only when the same item comes in several colours or sizes.
- Deal details โ how you hand the item over, how you accept payment, your city, your contact number and the full description.
- Review & publish โ a summary plus a checklist of anything still missing. Tap Publish listing.
Title
8 to 120 characters. Write it the way a buyer would search: brand, model, then the detail that matters โ โiPhone 13 128GB Midnight โ complete with boxโ beats โPhone for saleโ. As you type, HiveShop suggests the right category underneath.
Condition
| Condition | Means |
|---|---|
| Brand new | Unused, still sealed or with tags. |
| Like new | Opened but flawless โ used only a handful of times. |
| Good | Light signs of use, everything works perfectly. |
| Fair | Noticeable wear or a minor defect, still usable. |
| For parts | Not working โ sold for repair or spare parts. |
Price
Enter pesos only โ no commas needed. Under the field HiveShop shows what similar items in that category are going for (lowest, median and highest), so you can price competitively. Turn on Price is negotiable to invite offers, and use Compare-at price to show a struck-through โwasโ price.
๐ท Photos
- The first photo is the cover โ it is what buyers see in search results. Drag any photo to the first position to make it the cover.
- Reorder by dragging a thumbnail onto another one. The new order saves immediately.
- Remove with the โ on the thumbnail. This deletes the file permanently.
- Limit โ up to 10 photos per listing by default (an administrator can change this).
What makes a photo sell
- Daylight, plain background, the whole item in frame.
- One close-up of every flaw โ it prevents wasted trips and refund arguments.
- Include the box, charger, receipt or warranty card if you have them.
- Do not include your face, your address, or anything with your ID number on it.
โจ Smart fill (AI)
At the top of the Details step. Paste anything that describes your item โ the text you already wrote for a Facebook post, a chat message, or the spec blurb from a product page โ and tap Fill the form.
HiveShop reads it and fills the title, description, brand, condition, price, quantity, a suggested category and any specs it recognises. A status box below the button always tells you what happened; it never disappears on its own.
Smart fill is limited to 20 uses per hour per account. Nothing you type is lost if it fails: the form keeps whatever you had.
๐ Specifications & variants
Specifications
Each category carries its own set of fields โ storage and RAM for phones, size and colour for clothes, transmission and fuel for vehicles. They appear automatically once you pick a category, and a red * marks the ones that are required. Filling them in matters: buyers filter on these fields, so a listing with them set shows up in far more searches.
Variants
Use variants only when you are selling the same item in more than one option โ three colours of the same shirt, two storage tiers of the same phone. Do not use them for unrelated items; post those separately.
- Pick up to two options that vary (for example Colour and Size).
- Type each value and press Enter, or tap the suggested
+ valuechips. - A combination table appears. Set a price, stock count and SKU per row โ leave the price blank to use the main listing price.
- Apply price to all / Apply stock to all fill every row at once.
๐ค Deal details
| Field | What it does |
|---|---|
| Shipping / handover | Meet-up, courier, your own delivery, pick-up. Pick every option you will actually do โ buyers filter on it. |
| Payment accepted | COD, GCash, Maya, bank transfer, card, cash on meet-up. |
| City / province | Where the item is. Start typing and pick from the suggestions so your listing matches the โnear meโ filters. |
| Contact mobile | Required. Buyers get Call and SMS buttons for this number. Pre-filled from your profile. |
| Optional. Adds a WhatsApp button to your listing. | |
| Description | Up to 5000 characters. Cover what is included, why you are selling, every defect, and whether there is warranty left. |
๐ฆ My products
Dashboard โ My products lists everything you have posted, with view, favourite and inquiry counts on each. The chips along the top filter by status.
| Status | Meaning | Visible to buyers? |
|---|---|---|
| Active | Live and searchable. | Yes |
| Draft | Started but never published. | No |
| Pending review | Waiting for an administrator to approve it. | No |
| Reserved | You have a buyer lined up but the deal is not closed. | Yes, marked reserved |
| Sold | Deal done. | Yes, with a โSoldโ overlay |
| Expired | Past its listing period (60 days by default). | No โ until you renew |
| Inactive | You hid it. | No |
| Rejected | An administrator turned it down; the reason is shown on the card. | No |
Actions on each listing
- Edit โ reopens the full sell form with everything filled in.
- Mark reserved / Mark sold / Hide โ change status without editing.
- Relist as active โ bring a sold or hidden item back.
- Renew listing โ on an expired item; resets the expiry clock.
- Delete โ removes it from HiveShop. There is a confirmation and it cannot be undone.
๐ฌ Messages & offers
Signed-in buyers talk to sellers inside HiveShop. On any listing, Message seller opens a chat sheet (the button reads Continue chat once a thread already exists). Everything lands under Dashboard โ Messages, the first tab, and the โ๏ธ icon in the header carries a count of everything unread.
A buyer and a seller share a single thread per item, so nothing gets lost across listings.
The โฑ Offer button sends a price as a card in the thread instead of a plain sentence.
The first message emails and texts the seller; after that you are notified at most once every 30 minutes per thread.
Filter the thread list by role, or tick Archived to see threads you have put away.
- Threads with unread messages show a pink edge and a count; opening one marks it read.
- Messages refresh by themselves every few seconds while a thread is open.
- As the seller, the thread header carries Mark sold to <buyer> โ that records who bought the item and invites them to rate you.
- Archive hides a finished thread without deleting anything.
๐จ Inquiries
When a buyer sends a message from one of your listings it lands under Dashboard โ Inquiries, and you also get an email and an SMS if your contact details are set. The tab shows a red count of unread messages.
- A pink dot and a pink edge mark an unread message. Opening it marks it read.
- Expand a message for the buyer's contact details plus Call, SMS, WhatsApp or Email buttons โ whichever fits what they left.
- Type in the reply box and tap Send reply. HiveShop emails or texts your reply to the buyer and keeps it on the thread.
- Unread only filters the list down to what still needs an answer.
โญ Reviews & ratings
Sellers carry a โ average out of 5 with the number of reviews behind it. It shows on the product page's seller card, on the shop page header, and in chat โ tapping it jumps to the shop page's Seller reviews section.
| Question | Answer |
|---|---|
| Who can review a seller? | A signed-in buyer the seller marked as the buyer of that item, or a buyer who has a two-way chat with that seller on that listing. |
| Can I review myself? | No. Sellers never review their own listings. |
| How many reviews per item? | One per buyer per listing. You can edit it for 30 days. |
| Where do I write it? | On the product page โ the Rate this seller card at product.html?id=โฆ#review. Marking an item sold sends the buyer that link. |
| Can the seller answer? | Yes โ one public reply per review, from Dashboard โ Profile โ My reviews. |
| Can a review be removed? | Administrators can hide a review that breaks the rules; hidden reviews stop counting towards the average. |
๐ Promote a listing
Dashboard โ Promote lists every active listing with the ways to push it in front of more buyers. Bumping moves the item back to the top of โNewest firstโ; featuring pins it to the Featured rail and gives the card a โ badge.
| Option | Price | What it does |
|---|---|---|
| Free bump | Free, once every 7 days per listing | Back to the top of โNewest firstโ. The button shows a countdown until the next one is available. |
| Bump to top | โฑ29 | The same bump, any time, without waiting for the weekly one. |
| Featured for 7 days | โฑ149 | โ Featured badge plus a place in the featured rail for a week. |
| Featured for 30 days | โฑ399 | The same, for a month. Buying again extends the existing end date. |
Paying
- Tapping a paid option opens a secure PayMongo checkout (GCash, Maya or card). You return to the dashboard and the promotion goes live as soon as the payment clears.
- If the confirmation is slow, Check status on the order re-asks PayMongo.
- Pay manually creates an order with GCash instructions instead. Send the payment, upload a screenshot of the receipt, and an administrator activates it โ usually within 24 hours.
- Every order is listed under Order history with its reference number and status.
โค๏ธ Favorites
Tap the heart on any listing card or product page to save it. Dashboard โ Favorites shows everything you have saved, with the current price โ handy for watching whether a seller drops theirs. Tap the heart again to remove it.
๐ค Profile & account
Dashboard โ Profile holds everything buyers see about you plus your account settings.
| Section | What is in it |
|---|---|
| Stats | Active listings, sold count, total views, inquiries and favourites received. |
| Photo | Your avatar, cropped square and resized on your device before upload. |
| Name & shop name | The shop name is what appears on your listings and shop page; the name is for you. |
| About your shop | A short blurb โ what you sell, your hours, where you usually meet up. |
| City / province | Pre-fills every new listing. |
| Contact mobile / WhatsApp | Pre-fills the contact buttons on every new listing. |
| Default shipping / payment | Pre-selected chips on every new listing. |
| Sign-in methods | Which methods are linked, whether you have a password, and whether you are a verified seller. |
| Delete account | Closes the account and takes every active listing down. You must type DELETE to confirm. It cannot be undone. |
Theme
The avatar menu in the header (and Me on a phone) has a light/dark toggle. Your choice is remembered on that device and applies to every HiveShop page.
๐ชช Verified seller
A verified seller has had a government ID checked by a HiveShop administrator. Verified shops carry a โ on every listing and in search, and only they can switch on HiveShop Protect (below) so buyers can pay through escrow.
Getting verified
- Go to Dashboard โ Profile โ ๐ก๏ธ Verified seller.
- Type your full legal name exactly as printed on the ID, pick the ID type and enter the ID number.
- Take a photo of the front of the ID (and the back if it has one), plus a selfie holding the ID. On a phone the camera opens straight away.
- Tick the consent box and tap Submit for verification. The card then shows Under review.
- You get an SMS and an email with the decision, usually within one business day.
| Accepted IDs | Notes |
|---|---|
| PhilSys / National ID, UMID, driver's licence, passport, SSS, PRC, voter's, postal | Any one of them, as long as it is valid and not expired. |
| Anything else | Choose Other government ID โ the reviewer decides case by case. |
If you are rejected
The card shows the reviewer's note. Fix what it says โ usually a blurred photo, a cropped corner or a name that does not match โ and submit again. There is no limit on resubmissions, but only three per day. An administrator can also revoke a verification later if something turns out to be wrong; your Protect listings stop accepting new orders, and existing orders finish normally.
Payout method
Right under the verification card, ๐ธ Payout method is where the money from a Protect sale goes:
GCash, Maya or a bank account. GCash and Maya need an 11-digit number starting 09; a bank account
needs the bank name too. You need both a verification and a payout method before the Protect toggle
on the sell form unlocks.
๐ก๏ธ HiveShop Protect
HiveShop Protect is escrow. On a listing that shows the Protected shield, the buyer pays HiveShop instead of the seller. We hold the money and release it to the seller only once the buyer confirms the item arrived. Everything else on HiveShop is still arranged directly between the two of you.
For buyers
- On a protected listing, tap Buy with HiveShop Protect (you need to be signed in).
- The checkout sheet asks for the option and quantity, how you want it delivered, and โ for courier or seller delivery โ your address. Your last address is remembered on your device for next time.
- Check the breakdown (subtotal + shipping = total) and tap Pay securely. You land on PayMongo and can pay with GCash, Maya or a card.
- You come back to Dashboard โ Orders โ Buying. Follow the timeline: Paid โ Shipped โ Received โ Paid out.
- When the item arrives, tap Confirm receipt. If something is wrong, tap Open a dispute instead and the money stays held while an administrator looks at it.
For sellers
- Get verified and add a payout method.
- On the sell form's Deal details step, switch on ๐ก๏ธ HiveShop Protect and set a flat shipping fee (leave it at โฑ0 for meet-up).
- When a buyer pays, the order appears in Dashboard โ Orders โ Selling and the stock comes off your listing.
- Ship it, then tap Mark as shipped and enter the courier and tracking number. The buyer is notified straight away.
- Once the buyer confirms (or 7 days pass), the order becomes Payout due and HiveShop sends the money to your payout method, recording the reference on the order.
| What the buyer pays | What the seller gets |
|---|---|
| Item subtotal + the seller's flat shipping fee | Subtotal + shipping โ a 5% platform fee on the subtotal (minimum โฑ20) |
Before you ship you can still Cancel & refund the order โ the buyer is refunded in full and the stock goes back on the listing. After shipping, only an administrator can refund.
Order statuses
| Status | Meaning |
|---|---|
| Awaiting payment | Checkout opened, not paid yet. Expires after 2 hours. |
| Paid | HiveShop is holding the money. The seller should ship. |
| Shipped | On its way. The buyer has 7 days to confirm or dispute. |
| Completed | The buyer confirmed. The payout is due to the seller. |
| Paid out | HiveShop sent the money, with a reference on the order. |
| Disputed | The buyer raised a problem. The money stays held until an administrator decides. |
| Cancelled / Expired / Refunded | The order ended without a sale; any payment taken is returned. |
For administrators
- Admin โ Escrow shows every Protect order with three KPIs: money held in escrow, payouts due, and open disputes. Filter by status or search by order number.
- Open on a row shows the full fee breakdown, the delivery address, the PayMongo payment id and every event on the order.
- On a disputed order you either Release to seller or Refund the buyer (a real PayMongo refund; the stock is restored). A note is required either way.
- On a completed order, send the money yourself, then Mark as paid out with the transfer reference โ the seller is notified.
- On a paid or shipped order that has gone wrong (a seller who vanished), Refund the buyer refunds it in full.
- Admin โ Dashboard carries Protect GMV and Protect fees tiles plus a GMV-per-day chart.
โจ Fill from photos
Once at least one photo is uploaded on the sell form, a โจ Fill from photos button appears under the photo grid. It sends your first three photos to an AI vision model, which writes the listing for you.
Title, description, brand, condition, the category, and the category's specification fields.
A suggested range appears under the price box, next to the real range of similar listings on HiveShop. The price stays yours to set.
If the estimate is far off what similar items sell for, you get a warning instead of a silent guess.
Nothing is published. Every field it fills is a normal form field you can rewrite before you publish.
Getting a good read
- Shoot the whole item, in daylight, against a plain background.
- Include a photo of any label, model number or box โ that is where the brand and specs come from.
- Photograph defects too; the AI describes what it sees, and an honest listing gets fewer disputes.
It is limited to 15 runs per hour per account. The older โจ Smart fill box on the Details step still works if you would rather paste text than rely on photos โ and you can run both.
๐ก๏ธ Admin console
Administrators get an extra Admin entry in the avatar menu leading to admin.html. Anyone else who opens that URL sees a friendly โAdministrators onlyโ card โ no data is exposed.
| Tab | What it is for |
|---|---|
| ๐ Dashboard | KPI tiles (including Promo revenue) plus charts: views and inquiries per day, new products and users per day, listings by status, top categories, condition mix, the buyer funnel and promotion revenue per day. Switch between 7 / 30 / 90 days at the top right. |
| ๐ฆ Products | Every listing, filterable by search text, status, category, seller and flags. Approve, reject with a reason, feature, set the premium or YouTube link, open the full form, preview photos or delete. |
| ๐ฅ Users | Every account with listing counts and sign-in methods. Edit the name, shop name, role, verified tick and disabled flag. |
| ๐ฌ Inquiries | Every buyer message across the whole site, with read state and the seller's reply. |
| ๐ฉ Reports | The moderation queue โ see below. Includes โก Action + strike. |
| ๐ณ Sales | Promotion orders (PayMongo and manual): filter by status, open the uploaded payment proof, activate or cancel with a note. |
| โญ Reviews | Every seller review, filterable by seller ID, with hide / unhide. |
| ๐๏ธ Categories | The full category tree with the attribute schema and keyword editors. |
| โ๏ธ Settings | Site name, hero line, contact email, listing lifetime, photo limit, whether new listings need review, and the prohibited-items notice. |
| ๐ OTP logs | Every login-code request: identifier, channel, outcome, failure reason and IP. First place to look when someone cannot sign in. |
| ๐ Activity log | Who did what in this console, with a timestamp. |
๐ฑ Seed demo data at the top right inserts the demo sellers and sample listings. It is safe to run more than once โ it skips itself if the demo seller already exists.
๐ฉ Moderation & strikes
Approving listings
When Settings โ New listings need admin approval is on, everything published lands as Pending review. Filter Products by that status and use Approve or Reject. A rejection needs a reason, and that reason is shown to the seller on their dashboard and at the top of their edit form, so write something they can act on.
Reports
Buyers can report a listing as prohibited, counterfeit, a scam, wrongly categorised, offensive, or other. The Reports tab defaults to Open:
- Reviewed โ you have looked, nothing to do yet.
- Actioned โ you changed or removed the listing.
- Dismissed โ the report was not valid.
Each change can carry an admin note for the next person who looks. Reported listings show a ๐ฉ count in the Products table, and the Reported only filter narrows to them.
Strikes
A repeat offender needs more than a status change. Next to the three status buttons, โก Action + strike marks the report Actioned and adds one strike to the seller of the reported listing.
- The Users table has a Strikes column โ amber at one or two, red at three.
- At three strikes the account is disabled automatically and every one of that seller's active listings is switched to Inactive. The change is written to the admin activity log.
- If a strike was a mistake, Reset strikes on the user row puts the counter back to zero. Re-enabling a disabled account is a separate step in Edit user.
- Strikes are internal โ buyers never see a seller's strike count, only the โ rating.
Reviews
The Reviews tab lists every seller review with its rating, text and the seller's reply, newest first, and can be narrowed to one seller ID. Hide takes an abusive or off-topic review out of the public list and out of the seller's average; Unhide puts it back.
Sales
The Sales tab is the promotion ledger: every order with its reference, seller, listing, package, amount and status. Filter by status โ manual orders sit at Pending until you check the uploaded proof.
- View in the Proof column opens the receipt the seller uploaded.
- Activate applies the package to the listing right away โ the same code path a paid PayMongo webhook runs โ and can carry a note.
- Cancel closes an order that was never paid.
- The Dashboard tab carries a Promo revenue tile and a revenue-per-day chart.
Featuring
Feature pins a listing to the featured rail with a star badge, either until a date you pick or indefinitely. Unfeature removes it. Sellers can also buy this themselves โ see Promote a listing.
๐๏ธ Categories
The category tree drives search filters, the specification fields on the sell form, and the category suggester. Two levels: top-level categories and their sub-categories.
Keywords
A comma-separated list of the words buyers actually type โ synonyms, abbreviations and Filipino terms included (cellphone, cp, phone, iphone, samsung, telepono). The suggester scores a title against these, weighting longer keywords more heavily. The Test the category suggester box at the top of the tab lets you type a real title and see what comes back, so you can tune keywords until the right category wins.
Attribute schema
A JSON array describing the specification fields for that category. It is validated as you type โ a bad entry turns the box red and explains what is wrong, and Format JSON tidies it up.
[
{ "key": "storage", "label": "Storage", "type": "select",
"options": ["64GB","128GB","256GB","512GB","1TB"],
"required": true, "variant": true, "unit": "GB" },
{ "key": "color", "label": "Colour", "type": "select",
"options": ["Black","White","Blue","Red"], "variant": true }
]
| Key | Meaning |
|---|---|
key | Stored name. Keep it stable โ changing it orphans the values already saved on listings. |
label | What the seller sees. |
type | select, multiselect, text or number. |
options | Required for select and multiselect. |
required | Blocks publishing until it is filled in. |
variant | Lets sellers use it as a variant option (colour, size, storage, capacity). |
unit | Shown beside the label. |
โ FAQ & troubleshooting
My code never arrived
Check the number format (09XXXXXXXXX) and your spam folder for email codes. You can request a new code every 60 seconds. If it keeps failing, use a different method โ Google or email OTP โ and tell an administrator, who can see the outcome in the OTP logs.
My listing is not showing in search
Check its status on the dashboard. Drafts, pending, expired, hidden and rejected listings are not searchable. If it is Active, confirm the category and city are set โ most buyers arrive through a filter.
A photo will not upload
HEIC photos from an iPhone are rejected; set Camera โ Formats โ Most Compatible on the phone, or share the photo to yourself first to convert it to JPEG. Files above 15MB are also rejected before resizing.
Can I edit a listing after publishing?
Yes, any time and as often as you like โ Edit on the dashboard. Changes go live as soon as you save.
How long does a listing last?
60 days by default, then it becomes Expired and stops appearing in search. Renew listing puts it back with a fresh clock.
How do I install HiveShop on my phone?
It is a PWA. In Safari use Share โ Add to Home Screen; in Chrome use โฎ โ Install app. It then opens like an app, without the browser bar.
Who do I contact?
The contact email is on the Terms and Privacy pages, and administrators can change it under Settings.
HiveShop โ Developer Reference
Architecture, database schema, the full api.php / auth.php action contract, the photo pipeline and deployment notes for /var/www/html/shopping/.
๐งฑ Stack & conventions
| Path | /var/www/html/shopping/ โ https://vhivesolutions.com/shopping/ |
| Back end | PHP 8 (nginx + php-fpm), PDO/MySQL. No framework. |
| Database | shopping_db, user shopping_user@localhost with SELECT, INSERT, UPDATE, DELETE only. |
| Front end | Static HTML + vanilla ES modules. No bundler, no framework, no Bootstrap/Tailwind. |
| Prefixes | HS_ constants ยท hs*() PHP functions ยท hs_token_<slug> cookie ยท hs:<app path>:* localStorage keys (namespaced so two instances can share one origin) ยท hs- shared CSS ยท hss- seller CSS ยท hsa- admin CSS. |
| Auth | Token only โ no PHP sessions. Bearer header, or the hs_token HttpOnly cookie. sha256(token) is stored in user_sessions.token_hash; 365-day TTL. |
| Timezone | Asia/Manila, set in config/config.php. |
| Timestamps | BIGINT unix milliseconds via nowMs(). The front end formats them with timeAgo() / formatDate(). |
| Money | DECIMAL(12,2); rendered by formatPeso() as โฑ1,234. |
| Envelope | {success:true, data:โฆ} or {success:false, error:"โฆ"} with a 4xx/5xx status. api.js unwraps data and throws an ApiError otherwise. |
| Escaping | The server returns raw text; every string rendered into HTML passes through esc(). |
| Soft deletes | deleted_at everywhere; every public query filters deleted_at IS NULL. |
| Secrets | Only in config/config.php, which nginx denies. Never in HTML or JS. |
๐ณ File tree
/var/www/html/shopping/
โโโ index.html all products / storefront (public)
โโโ product.html product detail ?id= or ?slug= (public)
โโโ sell.html post or edit a listing ?id= (login)
โโโ dashboard.html seller area, 4 tabs by URL hash (login)
โโโ login.html 4-method sign-in (public)
โโโ admin.html admin console, 9 tabs (admin)
โโโ docs.html this page โ user + dev guide (public)
โโโ privacy.html ยท terms.html
โโโ api.php EVERY data action (?action=โฆ)
โโโ auth.php EVERY auth action (?action=โฆ)
โโโ .user.ini session.name = shopping_session
โโโ config/
โ โโโ config.php constants, getDB(), getCurrentUser(),
โ requireAuth(), requireAdmin(), hsSettings() [nginx-denied]
โโโ includes/
โ โโโ helpers.php params, pagination, slugs, shapes, rate limits
โ โโโ image.php upload validation + EXIF + WebP pipeline
โ โโโ sms.php SMSTrack bridge
โ โโโ mail.php MailTrack client
โ โโโ ai.php Anthropic Messages call + KeyWatch logging [nginx-denied]
โโโ database/
โ โโโ schema.sql ยท seed_categories.sql ยท seed_demo.sql [nginx-denied]
โโโ css/
โ โโโ hs.css THE design system: tokens, light + dark, every
โ โ shared component (.hs-*)
โ โโโ hs-seller.css sell.html + dashboard.html only (.hss-*)
โ โโโ hs-admin.css admin.html only (.hsa-*)
โโโ js/
โ โโโ api.js fetch wrapper + formatting helpers
โ โโโ ui.js shell, theme, toast, modal, sheet, product card
โ โโโ catalog.js index.html
โ โโโ product.js product.html
โ โโโ login.js login.html
โ โโโ sell.js sell.html
โ โโโ dashboard.js dashboard.html
โ โโโ admin.js admin.html
โโโ uploads/products/<product_id>/*.webp www-data, PHP execution denied
โโโ uploads/avatars/*.webp
โโโ pwa/manifest.json ยท pwa/icons/*.png
โโโ sw.js network-first; bypasses api.php, auth.php, uploads
โโโ README.md
hs.css is the single design system โ page styles never go in it. sell.html and dashboard.html load hs-seller.css after it; admin.html loads hs-admin.css. Both contain only what hs.css does not already provide, namespaced so they cannot collide.๐งฉ Front-end modules
Every page loads its module as <script type="module" src="js/x.js?v=20260927c">. Bump ?v= on both the CSS links and the script on every deploy.
js/api.js
| Export | Notes |
|---|---|
apiGet(action, params?) | GET api.php?action=โฆ, returns the unwrapped data. |
apiPost(action, body?) | POST with a JSON body. |
apiUpload(action, formData, onProgress?) | Multipart POST; with onProgress it uses XHR so the caller can draw a progress bar. |
authGet / authPost | The same against auth.php. |
getToken / setToken / clearToken | localStorage['hs:<app path>:hs_token'] via lsKey/lsGet/lsSet. A 401 clears it automatically. |
esc / escAttr | HTML escaping โ mandatory on every server string. |
formatPeso / formatNum / timeAgo / formatDate | Formatting; dates render in Asia/Manila. |
qs / qsAll / buildQuery / debounce | URL and timing helpers. |
ApiError | Error subclass carrying .status and .action. |
js/ui.js
| Export | Notes |
|---|---|
renderShell({active, search}) | Injects the header into #hs-header and the bottom nav into #hs-bottom-nav; resolves to the signed-in user or null. |
initTheme / toggleTheme | localStorage['hs:<app path>:hs_theme']; the theme is also painted inline in each page's <head> to avoid a flash (same key, built from location.pathname). |
loadUser / currentUser / requireLogin / logout | check_session is called once per page and memoised. |
toast / confirmModal / promptModal / openModal / openSheet | UI primitives. confirmModal(msg, {title, okText, danger}). |
productCard(p, opts) | The one product-card renderer. opts.here, opts.deltaBase, opts.showFav. |
lazyImages / renderEmpty / renderSkeletonCards / setLoading | List-rendering helpers. |
setMeta / setJsonLd | OG / Twitter tags and JSON-LD. |
statusLabel / conditionLabel / waUrl / telUrl / smsUrl / avatarHtml / initials | Small shared formatters. |
Page modules
- sell.js โ the listing form. Creates a
drafton the first photo or Continue soproduct_idexists for uploads; resizes each photo to 1600px JPEG q0.85 on canvas beforeupload_image; drag-reorder callsreorder_images; the attribute and variant UIs are generated fromget_category_schema; publish isupdate_productwithstatus:'active', then redirect toproduct.html?id=. - dashboard.js โ four tabs switched by the URL hash (
#products #inquiries #favorites #profile), each loaded lazily on first view. - admin.js โ nine tabs, also hash-driven. Charts are Chart.js 4.4.1 pinned from cdnjs, loaded
deferand awaited; they repaint through aMutationObserverondata-themeso light and dark each get their own validated palette.
๐๏ธ Database schema
shopping_db, InnoDB, utf8mb4_unicode_ci. Full DDL in database/schema.sql.
| Table | Key columns |
|---|---|
users | mobile, email, google_sub (all nullable + unique), name, shop_name, avatar_file, bio, city, province, contact_mobile, whatsapp, password_hash, role ENUM(user,admin), is_verified, is_disabled, default_shipping/default_payment JSON, created_at, last_login, deleted_at |
user_sessions | user_id, token_hash CHAR(64) UNIQUE, ua, ip, created_at, expires_at |
otp_codes | identifier, channel ENUM(sms,email), code_hash (bcrypt), expires_at, attempts, used_at |
otp_request_log | identifier, channel, ip, status ENUM(sent,failed,rate_limited), fail_reason |
login_attempts | identifier, ip, success โ throttle 8 failures / 15 min per identifier |
categories | parent_id, name, slug UNIQUE, icon (emoji), sort_order, keywords (CSV, feeds suggest_category), attribute_schema JSON, is_active |
products | seller_id, category_id, title, slug UNIQUE, description, brand, condition, price, compare_at_price, is_negotiable, quantity, sku, weight_g, dims, attributes JSON, variant_options JSON, shipping_options JSON, payment_options JSON, location_city/_province/lat/lng, contact fields, status, rejection_reason, is_featured, featured_until, premium_link, youtube_url (admin-only), the three counters, published_at, expires_at, deleted_at. FULLTEXT ft_search(title, description, brand). |
product_images | product_id, filename, thumb_filename, width, height, file_size, sort_order, is_primary, exif_json |
product_variants | option1_name/_value, option2_name/_value, price (NULL = product price), quantity, sku, image_id |
product_inquiries | product_id, seller_id, buyer_id, name, contact, message, is_read, seller_reply, replied_at |
product_favorites | UNIQUE(product_id, user_id) |
product_views | UNIQUE(product_id, viewer_key) โ one counted view per viewer per product per day |
analytics_events | event (search, filter, contact_click, share, favorite, sell_start, sell_publish, login), product_id, user_id, meta JSON |
product_reports | reason ENUM(prohibited, counterfeit, scam, wrong_category, offensive, other), details, status ENUM(open, reviewed, actioned, dismissed), admin_note |
site_settings | key PK / value โ site_name, hero_text, listing_days (60), max_photos (10), require_review (0), contact_email, prohibited_items_text |
admin_activity_log | admin_id, action, target_type, target_id, details JSON |
Status machine
A new listing is active, or pending_review when site_settings.require_review = 1. expires_at = published_at + listing_days; the daily cron flips overdue active rows to expired. A seller may set active, reserved, sold, inactive or draft; only an administrator can set pending_review, rejected or expired.
๐ฆ Product wire shape
Returned by every action that yields a product. Contact fields appear only on get_product (and for administrators in admin_products) โ never in public list rows.
{ id, slug, title, description, brand, condition, price, compare_at_price,
is_negotiable, quantity, sku,
category: { id, name, slug, parent_id, parent_name },
attributes: {}, variant_options: [ { name, values: [] } ],
variants: [ { id, option1_name, option1_value, option2_name, option2_value,
price, quantity, sku, image_id } ],
shipping_options: [], payment_options: [],
location_city, location_province, lat, lng,
contact_mobile, contact_whatsapp, contact_email, // detail only
protect_enabled, shipping_fee, // Phase 3 โ escrow
status, rejection_reason, is_featured, featured_until,
premium_link, youtube_url, // admin-only fields
views_count, favorites_count, inquiries_count,
published_at, expires_at, created_at, updated_at,
images: [ { id, url, thumb_url, width, height, is_primary, sort_order } ],
primary_thumb, // url string or null
seller: { id, name, shop_name, avatar_url, city, is_verified,
member_since, active_count },
is_favorited } // only with a viewer token
List endpoints return { items: [], total, page, per_page, pages }. Pagination params are page (1-based) and per_page (default 24, max 60).
๐ Public actions โ api.php
No token required. GET unless marked.
| Action | Params | Returns / notes |
|---|---|---|
get_products | q, category, min_price, max_price, condition (csv), city, province, seller_id, featured, sort (newestยทprice_ascยทprice_descยทpopular), page, per_page | Active, non-deleted rows only. FULLTEXT boolean mode on q with a LIKE fallback for short tokens. Logs a search event. |
get_product | id or slug | Full shape. A non-active listing is only returned to its owner or an admin, otherwise 404. |
get_similar | id, limit=12 | Same leaf category first, then the parent, ordered by price ASC, plus price_position. |
get_categories | โ | Nested tree: {id, name, slug, icon, parent_id, count_active, children[]}. |
get_category_schema | id | {category_id, name, attribute_schema, brands[], price_hint:{min,median,max,count}}. The schema is the child merged over the parent by key. |
suggest_category | title | Top 3 {id, name, parent_name, score} scored against categories.keywords. |
search_suggest | q | Up to 8 typeahead suggestions. |
check_premium_link | url | {valid} โ HEAD request, 8s timeout. |
get_site_settings | โ | Public subset: site_name, hero_text, listing_days, max_photos, contact_email, prohibited_items_text. |
get_cities | q | Flat array of city names โ distinct listing cities plus a static seed list. Max 30. |
submit_inquiry POST | product_id, name, contact, message | Rate-limited 5/hour/IP. Emails and texts the seller. |
track_view POST | product_id, referrer | Insert-ignore by viewer key; increments views_count only on a new row. |
track_event POST | event, product_id?, meta? | Allow-listed events only. |
report_product POST | product_id, reason, details, contact? | Rate-limited 3/hour/IP. |
๐ค Authenticated actions โ api.php
| Action | Params | Notes |
|---|---|---|
my_products | status (csv), page | The caller's own listings, drafts included, with counters. |
create_product POST | full product fields | Title 8โ120, description โค 5000, price โฅ 0, leaf category, enum condition, unknown attribute keys dropped, โค 2 variant options and โค 50 combinations. premium_link / youtube_url are ignored unless the caller is an admin. Slug is makeSlug(title)-id. |
update_product POST | id + fields | Owner or admin. Publishing a draft is this call with status:'active'; published_at is preserved on an already-published listing. |
set_product_status POST | id, status | Owner may set active ยท reserved ยท sold ยท inactive ยท draft. Re-activating an expired listing renews expires_at. |
delete_product POST | id | Soft delete. |
upload_image POST multipart | product_id, file | One file per call โ the UI uploads sequentially. Enforces max_photos. The first image becomes primary. Returns {id, url, thumb_url, width, height, file_size, is_primary, sort_order}. |
delete_image POST | image_id | Unlinks both files and reassigns primary if needed. |
reorder_images POST | product_id, image_ids[] | First id becomes the primary image. |
toggle_favorite POST | product_id | {favorited, favorites_count}. |
my_favorites | page | List rows. |
unread_counts auth | โ | {messages, inquiries, orders_action}. orders_action = orders waiting on the caller (seller: paid-not-shipped; buyer: shipped-not-confirmed) and feeds the header / bottom-nav dot. |
my_inquiries | page, unread? | Inquiries on the caller's listings, newest first, with product_title and product_thumb. The response also carries a top-level unread count. |
mark_inquiry_read POST | id | โ |
reply_inquiry POST | id, reply | Stores seller_reply and emails or texts the buyer. |
get_profile | โ | Includes has_password, default_shipping, default_payment and a stats block (active, sold, views, inquiries, favorites). |
update_profile POST | name, shop_name, bio, city, province, contact_mobile, whatsapp, default_shipping, default_payment | โ |
upload_avatar POST multipart | file | 512px max, 128px thumb. |
kyc_status auth | โ | Phase 3. {kyc_status, is_verified, verified_at, request:{id,status,id_type,id_number_last4,legal_name,admin_note,created_at,reviewed_at,has_front,has_back,has_selfie}|null, payout_method}. The ID number itself is never returned. |
kyc_submit POST multipart | legal_name, id_type, id_number, id_expiry?, birthdate?, front, back?, selfie? | One pending request at a time; 3 submissions/day. Runs a Claude Haiku vision pre-check and stores ai_json + ai_score. Never auto-approves. |
kyc_image self / admin | request_id, which=front|back|selfie | Raw image/webp bytes, Cache-Control: private, no-store. Needs the Bearer header, so the admin console fetches it as a blob and uses URL.createObjectURL โ it cannot be an <img src>. |
update_payout_method POST | type=gcash|maya|bank, account_name, account_number, bank_name? | GCash/Maya need an 11-digit 09โฆ number; bank needs bank_name. |
create_order POST | product_id, variant_id?, qty=1, shipping_method, address?, notes? | {order, checkout_url}. Refused on your own listing, a non-Protect listing, insufficient stock or an unverified seller. address is required for courier / own_delivery. |
verify_order POST | order_no | Re-asks PayMongo โ the fallback when the webhook is slow. |
my_orders auth | role=buying|selling|all, status?, page | Order rows plus product, buyer, seller and counts:{buying_open, selling_open}. |
get_order buyer / seller / admin | order_no | {order, events}. The buyer's address reaches the seller only once the order is paid. |
cancel_order POST | order_no, reason? | Buyer while pending_payment; seller while paid and not yet shipped (auto-refund + stock restored). |
ship_order POST | order_no, courier, tracking_no?, note? | Seller only, on a paid order. Notifies the buyer. |
confirm_receipt POST | order_no | Buyer only, on a shipped order โ completed; opens the review gate. |
dispute_order POST | order_no, reason | Buyer only, within 7 days of shipping. Emails the seller and the administrators. |
ai_parse_photos POST | product_id, image_ids[]? (โค 3) | Phase 3. Vision call over the first three processed WebP photos. Returns {title, description, brand, condition, category_hint, suggested_category, attributes, price_estimate:{low,high,currency}, confidence, warnings[]}. 15/hour/user, โค 4MB of image payload. |
ai_parse_product POST | text (โค 4000) | Smart fill. Server-side Anthropic Messages call (claude-haiku-4-5-20251001), logged through KeyWatch as app shopping. Rate limit 20/hour/user. Returns {title, description, brand, condition, price, quantity, category_hint, attributes, variant_options, suggested_category}. |
๐ก๏ธ Admin actions โ api.php
All behind requireAdmin(). Mutations write an admin_activity_log row.
| Action | Params | Returns / notes |
|---|---|---|
admin_analytics | days=30 | {days, kpis, series:{labels, views, new_products, inquiries, new_users}, top_categories, top_products, top_searches, funnel, status_breakdown, conditions, cities, revenue, escrow:{gmv_centavos, fees_centavos, orders_by_status, series}} |
admin_products | q, status, category, seller_id, featured, flagged, page | List rows plus seller, report_count and the contact fields. |
admin_update_product POST | id + any field, incl. status (any), is_featured, featured_until, premium_link, youtube_url, rejection_reason, category_id | โ |
admin_delete_product POST | id | Soft delete. |
admin_users | q, role, disabled, page | Users with products_total, products_active and a methods array (sms ยท email ยท google ยท password). |
admin_update_user POST | id, name, shop_name, role, is_verified, is_disabled | Cannot disable or demote yourself; cannot remove the last administrator. |
admin_inquiries | page, product_id? | Every inquiry site-wide. |
admin_otp_logs | page | From otp_request_log. |
admin_reports | status, page | Joined with the product title and status. |
admin_update_report POST | id, status, admin_note | โ |
admin_kyc | status=pending, page | Verification queue sorted ai_score DESC. Each item is the request summary plus user:{id,name,shop_name,email,mobile,rating_avg,strike_count,active_count}, ai_score and ai_json. |
admin_kyc_decide POST | request_id, decision=verify|reject|revoke, note? | verify โ is_verified=1; reject/revoke keep is_verified=0. SMS + email to the seller, logged to the activity log. |
admin_orders | status?, q?, page | Escrow orders plus kpis:{held_centavos, payouts_due_centavos, disputes_open}. |
admin_resolve_dispute POST | order_no, resolution=refunded|released, note | refunded โ PayMongo refund + stock restored; released โ completed. |
admin_mark_paid_out POST | order_no, payout_ref, note? | completed โ paid_out; notifies the seller. |
admin_refund_order POST | order_no, note | Full refund of a paid / shipped order โ the seller-no-show escape hatch. |
admin_categories | โ | Flat array (not a tree) with product_count; admin.js nests it client-side. |
admin_save_category POST | id?, name, slug?, parent_id, icon, sort_order, keywords, attribute_schema, is_active | Insert when id is absent. A duplicate slug returns a friendly error. |
admin_delete_category POST | id | Refused while any listing references it. |
admin_settings | โ | Flat key โ value map of every site_settings row merged over the defaults. |
admin_update_settings POST | any settings keys | โ |
admin_activity | page | Joined with the admin's name. |
admin_seed_demo POST | โ | Runs database/seed_demo.sql. Idempotent: returns {ok:true, skipped:true, reason} when the demo seller already exists. |
๐ auth.php
| Action | Params | Notes |
|---|---|---|
request_otp POST | mobile | Normalises 09xx โ +639xx. bcrypt-hashed 6-digit code, 10-minute expiry, 1 send / 60s per identifier and 5 / hour per IP. Sent through the SMSTrack bridge. |
verify_otp POST | mobile, code | Max 5 attempts per code. Upserts the user by mobile, creates the session, sets the cookie, returns {token, user}. |
request_email_login / verify_email_login POST | email (+ code) | Same flow by email through MailTrack. Subject: Your HiveShop login code. |
password_login POST | identifier, password | Throttled through login_attempts โ 8 failures / 15 min per identifier. |
set_password POST auth | password (โฅ 8) | โ |
google_login POST | credential | GIS JS-callback mode only, never redirect mode. Verified against oauth2.googleapis.com/tokeninfo, checking aud and email_verified. Upserts by google_sub, then by email. |
check_session | โ | {user} or 401. |
logout POST | โ | Deletes the session row and clears the cookie. |
delete_account POST auth | โ | Soft-deletes the user and sets every active listing to inactive. |
New accounts from any method get role = 'user'. users.name defaults to Seller <last 4 of mobile> or the email local-part until it is set.
๐ผ๏ธ Photo pipeline
Client side (sell.js / dashboard.js)
- Filter to
image/*and check the remaining quota againstmax_photos. - Draw to a canvas at 1600px on the long edge (never upscaling) and export JPEG at q0.85. Avatars are centre-cropped square at 512px.
apiUpload('upload_image', fd, onProgress)one file at a time, with a per-tile progress bar.
Server side (includes/image.php)
UPLOAD_ERR_OKโis_uploaded_file()โ โค 15MB โmime_content_type()in jpeg/png/webp/gif (HEIC rejected with a clear message) โ extension allow-list. Read from tmp only.exif_read_data()once: fix orientation 3/6/8 and collect the curated EXIF subset intoexif_json.- Resize to 1600px long edge,
imagewebp(q=82)โuploads/products/<pid>/img_<ms>_<8hex>.webp. - Thumbnail from the resized image at 480px,
imagewebp(q=80)โโฆ_thumb.webp. - The original is never stored. GD only โ Imagick is not installed on this VPS. Dirs 0755, files 0644, owner
www-data.
.php, .phtml and .phar under /shopping/uploads/. Verify with curl after any nginx change.๐ Deploy & nginx
nginx โ inside the vhivesolutions server block
# โโ HiveShop marketplace (/shopping) โโ
location ^~ /shopping/config/ { deny all; }
location ^~ /shopping/includes/ { deny all; }
location ^~ /shopping/database/ { deny all; }
location ^~ /shopping/backups/ { deny all; }
location ~* ^/shopping/.*\.(md|sql|ini|log)$ { deny all; }
location ~* ^/shopping/uploads/.*\.(php|phtml|phar)$ { deny all; }
location ^~ /shopping/uploads/ { expires 30d; add_header Cache-Control "public"; }
location = /shopping/sw.js { add_header Cache-Control "no-store"; }
nginx -t && systemctl reload nginx
sites-available/vhivesolutions, not default
nginx ignores .htaccess entirely, and a new prefix location can bypass a sibling deny. After every change, curl each denied path and confirm a 403.Deploy checklist
- Bump
?v=on every CSS link and module script across all pages. chown -R www-data:www-data /var/www/html/shopping;uploads/writable.- Verify with curl:
config/config.php,includes/helpers.php,database/schema.sqlandREADME.mdeach return 403. - Open every page in both themes at 375px, 768px and 1280px with the console open โ zero errors is the bar.
- Cross-check that every
?action=the front end calls exists inapi.php/auth.php. - Log the deploy:
php /var/www/html/journal/cli/log.php --type=build --app=shopping --title="โฆ".
Integrations
| SMS | Only through the SMSTrack bridge โ POST https://vhivesolutions.com/smstrack/api.php with action=send. Key in HS_SMSTRACK_API_KEY. |
Only through /var/www/html/mailtrack/client/mail_client.php โ sendTrackedEmail(). Key in HS_MAILTRACK_API_KEY. | |
| AI | Anthropic Messages API, claude-haiku-4-5-20251001, key in HS_ANTHROPIC_API_KEY. Every call is logged through KeyWatch (/var/www/html/apikey/client/usage_logger.php) as app shopping. |
GIS JS-callback mode only. Client ID 974353825718-c3t4c2ilcn2t55gi7o61s27htcj3e8ae.apps.googleusercontent.com; vhivesolutions.com is an authorised origin. | |
| Charts | Chart.js 4.4.1 pinned from cdnjs, admin only. The console degrades to tiles and tables if the CDN is blocked. |
โฐ Cron & maintenance
One scheduled job, keyed so it cannot be triggered from outside:
# root crontab โ 03:10 Asia/Manila
10 3 * * * curl -s "https://vhivesolutions.com/shopping/api.php?action=cron_expire&key=$HS_CRON_KEY" >/dev/null
It flips overdue active listings to expired. Sellers renew them with one tap from the dashboard.
Routine checks
- OTP logs โ a run of
failedrows usually means the SMSTrack key or credit, not the app. - Reports โ keep the open queue at zero.
- Disk โ
uploads/products/grows with every listing; deleted listings are soft-deleted and their files stay until pruned. - KeyWatch โ watch Smart fill spend under app
shopping.
Useful queries
-- listings by status
SELECT status, COUNT(*) FROM products WHERE deleted_at IS NULL GROUP BY status;
-- promote a user to administrator
UPDATE users SET role = 'admin' WHERE email = 'someone@example.com';
-- orphaned image rows
SELECT i.* FROM product_images i
LEFT JOIN products p ON p.id = i.product_id WHERE p.id IS NULL;