๐Ÿ HiveShop
v1.0 ยท 2026-09-26
โ† Back to the marketplace Product Quotation Investors

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.

๐Ÿ‡ต๐Ÿ‡ญ Philippine marketplace๐Ÿ“ฑ Works on any phone๐Ÿ” Four ways to sign in๐ŸŒ— Light & dark

๐Ÿ 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.

๐Ÿ”Ž
Browse freely

Search, filter by category, price, condition and city, and open any listing โ€” no account needed.

๐Ÿ“ธ
Post in minutes

Photos are resized on your phone before upload, so listing works even on a slow connection.

โœจ
Smart fill

Paste a description and AI fills the title, price, condition, category and specs for you.

๐Ÿ’ฌ
Direct contact

Buyers reach you by call, SMS, WhatsApp or an on-site inquiry. HiveShop does not handle payment.

โš ๏ธ
HiveShop does not process payments Every deal is arranged directly between buyer and seller. Meet in a public place, inspect the item before paying, and never send a deposit to someone you have not met.

๐Ÿ” 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.

MethodHow it worksWhen to use it
SMS OTPEnter your PH mobile (09XXXXXXXXX); a 6-digit code arrives by text and is valid for 10 minutes.The default โ€” fastest on a phone.
Email OTPEnter your email; the same kind of 6-digit code arrives by email.No load, or you prefer email.
PasswordEmail or mobile plus a password you set in your profile.Fastest once set up; needed on a desktop without your phone.
GoogleThe Google button below the tabs โ€” one tap, no code.If your Google account uses the same email.
๐Ÿ’ก
Set a password You start without one. Add it under Dashboard โ†’ Profile โ†’ Sign-in methods so you always have a fallback if you change phones.
๐Ÿ”’
Codes are single-use A code expires after 10 minutes and allows 5 tries. You can request a new one every 60 seconds. HiveShop will never ask you for your code โ€” nobody from support needs it.

๐Ÿท๏ธ 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.

  1. Photos โ€” drag files in, tap to browse, or use Take a photo. The first photo is the cover.
  2. Details โ€” title, category, brand, condition, price and quantity. Optional Smart fill box at the top can do most of this for you.
  3. Specifications โ€” the fields shown here depend on the category you picked (storage for a phone, size for shoes, and so on).
  4. Variants โ€” optional. Use it only when the same item comes in several colours or sizes.
  5. Deal details โ€” how you hand the item over, how you accept payment, your city, your contact number and the full description.
  6. Review & publish โ€” a summary plus a checklist of anything still missing. Tap Publish listing.
๐Ÿ’พ
Nothing is lost if you stop halfway As soon as you add your first photo (or tap Continue) HiveShop saves a Draft. Drafts are private โ€” only you see them, under Dashboard โ†’ My products โ†’ Drafts. Save draft at the bottom saves at any time, and the bar tells you whether you have unsaved changes.

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

ConditionMeans
Brand newUnused, still sealed or with tags.
Like newOpened but flawless โ€” used only a handful of times.
GoodLight signs of use, everything works perfectly.
FairNoticeable wear or a minor defect, still usable.
For partsNot 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).
๐Ÿ“ถ
Your phone shrinks the photo first Before anything is sent, each photo is resized to 1600px on the long edge right on your device. A 6MB camera photo typically uploads as roughly 300KB, so listing works on mobile data. The server then converts it to WebP and makes a thumbnail.

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.

๐Ÿ‘€
Always check what it filled in Smart fill is a time-saver, not an oracle. It can misread a price, pick a neighbouring category, or invent a spec. Read every field before you publish โ€” you are responsible for what your listing says.

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.

  1. Pick up to two options that vary (for example Colour and Size).
  2. Type each value and press Enter, or tap the suggested + value chips.
  3. A combination table appears. Set a price, stock count and SKU per row โ€” leave the price blank to use the main listing price.
  4. Apply price to all / Apply stock to all fill every row at once.
๐Ÿ”ข
Limits At most 2 options and 50 combinations per listing.

๐Ÿค Deal details

FieldWhat it does
Shipping / handoverMeet-up, courier, your own delivery, pick-up. Pick every option you will actually do โ€” buyers filter on it.
Payment acceptedCOD, GCash, Maya, bank transfer, card, cash on meet-up.
City / provinceWhere the item is. Start typing and pick from the suggestions so your listing matches the โ€œnear meโ€ filters.
Contact mobileRequired. Buyers get Call and SMS buttons for this number. Pre-filled from your profile.
WhatsAppOptional. Adds a WhatsApp button to your listing.
DescriptionUp to 5000 characters. Cover what is included, why you are selling, every defect, and whether there is warranty left.
โš™๏ธ
Set your defaults once Under Dashboard โ†’ Profile you can set default shipping and payment options plus your city and contact number. Every new listing starts pre-filled with them.

๐Ÿ“ฆ 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.

StatusMeaningVisible to buyers?
ActiveLive and searchable.Yes
DraftStarted but never published.No
Pending reviewWaiting for an administrator to approve it.No
ReservedYou have a buyer lined up but the deal is not closed.Yes, marked reserved
SoldDeal done.Yes, with a โ€œSoldโ€ overlay
ExpiredPast its listing period (60 days by default).No โ€” until you renew
InactiveYou hid it.No
RejectedAn 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.

๐Ÿงต
One thread per listing

A buyer and a seller share a single thread per item, so nothing gets lost across listings.

โ‚ฑ
Make an offer

The โ‚ฑ Offer button sends a price as a card in the thread instead of a plain sentence.

๐Ÿ””
Notified once

The first message emails and texts the seller; after that you are notified at most once every 30 minutes per thread.

๐Ÿ—‚๏ธ
Buying / Selling

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.
โš ๏ธ
Still no payments on HiveShop An offer in chat is a conversation, not a transaction. Meet in a public place and inspect the item before any money changes hands.

๐Ÿ“จ 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.
โฑ๏ธ
Answer fast Most buyers on classifieds message several sellers at once; the first useful reply usually wins the sale.

โญ 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.

QuestionAnswer
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.

OptionPriceWhat it does
Free bumpFree, once every 7 days per listingBack to the top of โ€œNewest firstโ€. The button shows a countdown until the next one is available.
Bump to topโ‚ฑ29The 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โ‚ฑ399The 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.
๐Ÿ’ก
Bump before the weekend Traffic peaks Friday evening through Sunday, so a bump on Friday afternoon gets the most eyes for the money.

โค๏ธ 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.

SectionWhat is in it
StatsActive listings, sold count, total views, inquiries and favourites received.
PhotoYour avatar, cropped square and resized on your device before upload.
Name & shop nameThe shop name is what appears on your listings and shop page; the name is for you.
About your shopA short blurb โ€” what you sell, your hours, where you usually meet up.
City / provincePre-fills every new listing.
Contact mobile / WhatsAppPre-fills the contact buttons on every new listing.
Default shipping / paymentPre-selected chips on every new listing.
Sign-in methodsWhich methods are linked, whether you have a password, and whether you are a verified seller.
Delete accountCloses 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

  1. Go to Dashboard โ†’ Profile โ†’ ๐Ÿ›ก๏ธ Verified seller.
  2. Type your full legal name exactly as printed on the ID, pick the ID type and enter the ID number.
  3. 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.
  4. Tick the consent box and tap Submit for verification. The card then shows Under review.
  5. You get an SMS and an email with the decision, usually within one business day.
Accepted IDsNotes
PhilSys / National ID, UMID, driver's licence, passport, SSS, PRC, voter's, postalAny one of them, as long as it is valid and not expired.
Anything elseChoose Other government ID โ€” the reviewer decides case by case.
๐Ÿ”
Your privacy โ€” RA 10173 (Data Privacy Act of 2012) Your ID is collected only to confirm who you are. The ID number is stored encrypted and only its last four digits are ever shown again. The images are never served publicly โ€” only a HiveShop administrator can open them, through an authenticated endpoint, while reviewing your request. All ID images are permanently deleted 30 days after the decision, and only the last 4 digits and the review summary stay on file. See the Privacy Policy.

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

  1. On a protected listing, tap Buy with HiveShop Protect (you need to be signed in).
  2. 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.
  3. Check the breakdown (subtotal + shipping = total) and tap Pay securely. You land on PayMongo and can pay with GCash, Maya or a card.
  4. You come back to Dashboard โ†’ Orders โ†’ Buying. Follow the timeline: Paid โ†’ Shipped โ†’ Received โ†’ Paid out.
  5. 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.
โณ
Deadlines An unpaid order expires after 2 hours. After a seller ships, you have 7 days to confirm or dispute โ€” after that HiveShop completes the order automatically and pays the seller.

For sellers

  1. Get verified and add a payout method.
  2. 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).
  3. When a buyer pays, the order appears in Dashboard โ†’ Orders โ†’ Selling and the stock comes off your listing.
  4. Ship it, then tap Mark as shipped and enter the courier and tracking number. The buyer is notified straight away.
  5. 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 paysWhat the seller gets
Item subtotal + the seller's flat shipping feeSubtotal + 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

StatusMeaning
Awaiting paymentCheckout opened, not paid yet. Expires after 2 hours.
PaidHiveShop is holding the money. The seller should ship.
ShippedOn its way. The buyer has 7 days to confirm or dispute.
CompletedThe buyer confirmed. The payout is due to the seller.
Paid outHiveShop sent the money, with a reference on the order.
DisputedThe buyer raised a problem. The money stays held until an administrator decides.
Cancelled / Expired / RefundedThe 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.
โš ๏ธ
Protect is not on every listing Listings without the shield are ordinary classifieds: HiveShop never touches the money, so meet in a public place and inspect the item before you pay.

โœจ 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.

๐Ÿ“
What it fills

Title, description, brand, condition, the category, and the category's specification fields.

๐Ÿ’ฐ
Price estimate

A suggested range appears under the price box, next to the real range of similar listings on HiveShop. The price stays yours to set.

โš ๏ธ
Warnings

If the estimate is far off what similar items sell for, you get a warning instead of a silent guess.

๐Ÿ”
Always editable

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.

๐Ÿ’ก
Always read it back The AI can misread a model number or guess the wrong storage size. You are responsible for what your listing says, so check every field before publishing.

๐Ÿ›ก๏ธ 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.

TabWhat it is for
๐Ÿ“Š DashboardKPI 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.
๐Ÿ“ฆ ProductsEvery 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.
๐Ÿ‘ฅ UsersEvery account with listing counts and sign-in methods. Edit the name, shop name, role, verified tick and disabled flag.
๐Ÿ’ฌ InquiriesEvery buyer message across the whole site, with read state and the seller's reply.
๐Ÿšฉ ReportsThe moderation queue โ€” see below. Includes โšก Action + strike.
๐Ÿ’ณ SalesPromotion orders (PayMongo and manual): filter by status, open the uploaded payment proof, activate or cancel with a note.
โญ ReviewsEvery seller review, filterable by seller ID, with hide / unhide.
๐Ÿ—‚๏ธ CategoriesThe full category tree with the attribute schema and keyword editors.
โš™๏ธ SettingsSite name, hero line, contact email, listing lifetime, photo limit, whether new listings need review, and the prohibited-items notice.
๐Ÿ”‘ OTP logsEvery login-code request: identifier, channel, outcome, failure reason and IP. First place to look when someone cannot sign in.
๐Ÿ“œ Activity logWho 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.

๐Ÿ›ก๏ธ
Guardrails You cannot change your own role or disable your own account, and the last remaining administrator cannot be demoted. A category can only be deleted when no listing uses it.

๐Ÿšฉ 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 }
]
KeyMeaning
keyStored name. Keep it stable โ€” changing it orphans the values already saved on listings.
labelWhat the seller sees.
typeselect, multiselect, text or number.
optionsRequired for select and multiselect.
requiredBlocks publishing until it is filled in.
variantLets sellers use it as a variant option (colour, size, storage, capacity).
unitShown beside the label.
๐Ÿงฌ
Sub-categories inherit A sub-category starts from its parent's schema; anything it defines is merged over the parent's by key. So put the general fields on the parent and only the specifics on the child.

โ“ 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/.

PHP 8 + MySQLVanilla ES modulesNo build stepToken authunix-ms timestamps

๐Ÿงฑ Stack & conventions

Path/var/www/html/shopping/ โ†’ https://vhivesolutions.com/shopping/
Back endPHP 8 (nginx + php-fpm), PDO/MySQL. No framework.
Databaseshopping_db, user shopping_user@localhost with SELECT, INSERT, UPDATE, DELETE only.
Front endStatic HTML + vanilla ES modules. No bundler, no framework, no Bootstrap/Tailwind.
PrefixesHS_ 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.
AuthToken only โ€” no PHP sessions. Bearer header, or the hs_token HttpOnly cookie. sha256(token) is stored in user_sessions.token_hash; 365-day TTL.
TimezoneAsia/Manila, set in config/config.php.
TimestampsBIGINT unix milliseconds via nowMs(). The front end formats them with timeAgo() / formatDate().
MoneyDECIMAL(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.
EscapingThe server returns raw text; every string rendered into HTML passes through esc().
Soft deletesdeleted_at everywhere; every public query filters deleted_at IS NULL.
SecretsOnly 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
๐Ÿšง
CSS ownership 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

ExportNotes
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 / authPostThe same against auth.php.
getToken / setToken / clearTokenlocalStorage['hs:<app path>:hs_token'] via lsKey/lsGet/lsSet. A 401 clears it automatically.
esc / escAttrHTML escaping โ€” mandatory on every server string.
formatPeso / formatNum / timeAgo / formatDateFormatting; dates render in Asia/Manila.
qs / qsAll / buildQuery / debounceURL and timing helpers.
ApiErrorError subclass carrying .status and .action.

js/ui.js

ExportNotes
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 / toggleThemelocalStorage['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 / logoutcheck_session is called once per page and memoised.
toast / confirmModal / promptModal / openModal / openSheetUI primitives. confirmModal(msg, {title, okText, danger}).
productCard(p, opts)The one product-card renderer. opts.here, opts.deltaBase, opts.showFav.
lazyImages / renderEmpty / renderSkeletonCards / setLoadingList-rendering helpers.
setMeta / setJsonLdOG / Twitter tags and JSON-LD.
statusLabel / conditionLabel / waUrl / telUrl / smsUrl / avatarHtml / initialsSmall shared formatters.

Page modules

  • sell.js โ€” the listing form. Creates a draft on the first photo or Continue so product_id exists for uploads; resizes each photo to 1600px JPEG q0.85 on canvas before upload_image; drag-reorder calls reorder_images; the attribute and variant UIs are generated from get_category_schema; publish is update_product with status:'active', then redirect to product.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 defer and awaited; they repaint through a MutationObserver on data-theme so light and dark each get their own validated palette.

๐Ÿ—„๏ธ Database schema

shopping_db, InnoDB, utf8mb4_unicode_ci. Full DDL in database/schema.sql.

TableKey columns
usersmobile, 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_sessionsuser_id, token_hash CHAR(64) UNIQUE, ua, ip, created_at, expires_at
otp_codesidentifier, channel ENUM(sms,email), code_hash (bcrypt), expires_at, attempts, used_at
otp_request_logidentifier, channel, ip, status ENUM(sent,failed,rate_limited), fail_reason
login_attemptsidentifier, ip, success โ€” throttle 8 failures / 15 min per identifier
categoriesparent_id, name, slug UNIQUE, icon (emoji), sort_order, keywords (CSV, feeds suggest_category), attribute_schema JSON, is_active
productsseller_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_imagesproduct_id, filename, thumb_filename, width, height, file_size, sort_order, is_primary, exif_json
product_variantsoption1_name/_value, option2_name/_value, price (NULL = product price), quantity, sku, image_id
product_inquiriesproduct_id, seller_id, buyer_id, name, contact, message, is_read, seller_reply, replied_at
product_favoritesUNIQUE(product_id, user_id)
product_viewsUNIQUE(product_id, viewer_key) โ€” one counted view per viewer per product per day
analytics_eventsevent (search, filter, contact_click, share, favorite, sell_start, sell_publish, login), product_id, user_id, meta JSON
product_reportsreason ENUM(prohibited, counterfeit, scam, wrong_category, offensive, other), details, status ENUM(open, reviewed, actioned, dismissed), admin_note
site_settingskey PK / value โ€” site_name, hero_text, listing_days (60), max_photos (10), require_review (0), contact_email, prohibited_items_text
admin_activity_logadmin_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.

ActionParamsReturns / notes
get_productsq, category, min_price, max_price, condition (csv), city, province, seller_id, featured, sort (newestยทprice_ascยทprice_descยทpopular), page, per_pageActive, non-deleted rows only. FULLTEXT boolean mode on q with a LIKE fallback for short tokens. Logs a search event.
get_productid or slugFull shape. A non-active listing is only returned to its owner or an admin, otherwise 404.
get_similarid, limit=12Same 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_schemaid{category_id, name, attribute_schema, brands[], price_hint:{min,median,max,count}}. The schema is the child merged over the parent by key.
suggest_categorytitleTop 3 {id, name, parent_name, score} scored against categories.keywords.
search_suggestqUp to 8 typeahead suggestions.
check_premium_linkurl{valid} โ€” HEAD request, 8s timeout.
get_site_settingsโ€”Public subset: site_name, hero_text, listing_days, max_photos, contact_email, prohibited_items_text.
get_citiesqFlat array of city names โ€” distinct listing cities plus a static seed list. Max 30.
submit_inquiry POSTproduct_id, name, contact, messageRate-limited 5/hour/IP. Emails and texts the seller.
track_view POSTproduct_id, referrerInsert-ignore by viewer key; increments views_count only on a new row.
track_event POSTevent, product_id?, meta?Allow-listed events only.
report_product POSTproduct_id, reason, details, contact?Rate-limited 3/hour/IP.

๐Ÿ‘ค Authenticated actions โ€” api.php

ActionParamsNotes
my_productsstatus (csv), pageThe caller's own listings, drafts included, with counters.
create_product POSTfull product fieldsTitle 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 POSTid + fieldsOwner or admin. Publishing a draft is this call with status:'active'; published_at is preserved on an already-published listing.
set_product_status POSTid, statusOwner may set active ยท reserved ยท sold ยท inactive ยท draft. Re-activating an expired listing renews expires_at.
delete_product POSTidSoft delete.
upload_image POST multipartproduct_id, fileOne 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 POSTimage_idUnlinks both files and reassigns primary if needed.
reorder_images POSTproduct_id, image_ids[]First id becomes the primary image.
toggle_favorite POSTproduct_id{favorited, favorites_count}.
my_favoritespageList 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_inquiriespage, 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 POSTidโ€”
reply_inquiry POSTid, replyStores 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 POSTname, shop_name, bio, city, province, contact_mobile, whatsapp, default_shipping, default_paymentโ€”
upload_avatar POST multipartfile512px 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 multipartlegal_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 / adminrequest_id, which=front|back|selfieRaw 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 POSTtype=gcash|maya|bank, account_name, account_number, bank_name?GCash/Maya need an 11-digit 09โ€ฆ number; bank needs bank_name.
create_order POSTproduct_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 POSTorder_noRe-asks PayMongo โ€” the fallback when the webhook is slow.
my_orders authrole=buying|selling|all, status?, pageOrder rows plus product, buyer, seller and counts:{buying_open, selling_open}.
get_order buyer / seller / adminorder_no{order, events}. The buyer's address reaches the seller only once the order is paid.
cancel_order POSTorder_no, reason?Buyer while pending_payment; seller while paid and not yet shipped (auto-refund + stock restored).
ship_order POSTorder_no, courier, tracking_no?, note?Seller only, on a paid order. Notifies the buyer.
confirm_receipt POSTorder_noBuyer only, on a shipped order โ†’ completed; opens the review gate.
dispute_order POSTorder_no, reasonBuyer only, within 7 days of shipping. Emails the seller and the administrators.
ai_parse_photos POSTproduct_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 POSTtext (โ‰ค 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.

ActionParamsReturns / notes
admin_analyticsdays=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_productsq, status, category, seller_id, featured, flagged, pageList rows plus seller, report_count and the contact fields.
admin_update_product POSTid + any field, incl. status (any), is_featured, featured_until, premium_link, youtube_url, rejection_reason, category_idโ€”
admin_delete_product POSTidSoft delete.
admin_usersq, role, disabled, pageUsers with products_total, products_active and a methods array (sms ยท email ยท google ยท password).
admin_update_user POSTid, name, shop_name, role, is_verified, is_disabledCannot disable or demote yourself; cannot remove the last administrator.
admin_inquiriespage, product_id?Every inquiry site-wide.
admin_otp_logspageFrom otp_request_log.
admin_reportsstatus, pageJoined with the product title and status.
admin_update_report POSTid, status, admin_noteโ€”
admin_kycstatus=pending, pageVerification 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 POSTrequest_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_ordersstatus?, q?, pageEscrow orders plus kpis:{held_centavos, payouts_due_centavos, disputes_open}.
admin_resolve_dispute POSTorder_no, resolution=refunded|released, noterefunded โ†’ PayMongo refund + stock restored; released โ†’ completed.
admin_mark_paid_out POSTorder_no, payout_ref, note?completed โ†’ paid_out; notifies the seller.
admin_refund_order POSTorder_no, noteFull 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 POSTid?, name, slug?, parent_id, icon, sort_order, keywords, attribute_schema, is_activeInsert when id is absent. A duplicate slug returns a friendly error.
admin_delete_category POSTidRefused while any listing references it.
admin_settingsโ€”Flat key โ†’ value map of every site_settings row merged over the defaults.
admin_update_settings POSTany settings keysโ€”
admin_activitypageJoined 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

ActionParamsNotes
request_otp POSTmobileNormalises 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 POSTmobile, codeMax 5 attempts per code. Upserts the user by mobile, creates the session, sets the cookie, returns {token, user}.
request_email_login / verify_email_login POSTemail (+ code)Same flow by email through MailTrack. Subject: Your HiveShop login code.
password_login POSTidentifier, passwordThrottled through login_attempts โ€” 8 failures / 15 min per identifier.
set_password POST authpassword (โ‰ฅ 8)โ€”
google_login POSTcredentialGIS 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)

  1. Filter to image/* and check the remaining quota against max_photos.
  2. 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.
  3. apiUpload('upload_image', fd, onProgress) one file at a time, with a per-tile progress bar.

Server side (includes/image.php)

  1. 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.
  2. exif_read_data() once: fix orientation 3/6/8 and collect the curated EXIF subset into exif_json.
  3. Resize to 1600px long edge, imagewebp(q=82) โ†’ uploads/products/<pid>/img_<ms>_<8hex>.webp.
  4. Thumbnail from the resized image at 480px, imagewebp(q=80) โ†’ โ€ฆ_thumb.webp.
  5. The original is never stored. GD only โ€” Imagick is not installed on this VPS. Dirs 0755, files 0644, owner www-data.
๐Ÿ”
Uploads must never execute nginx denies .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
๐Ÿ“„
Edit 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

  1. Bump ?v= on every CSS link and module script across all pages.
  2. chown -R www-data:www-data /var/www/html/shopping; uploads/ writable.
  3. Verify with curl: config/config.php, includes/helpers.php, database/schema.sql and README.md each return 403.
  4. Open every page in both themes at 375px, 768px and 1280px with the console open โ€” zero errors is the bar.
  5. Cross-check that every ?action= the front end calls exists in api.php / auth.php.
  6. Log the deploy: php /var/www/html/journal/cli/log.php --type=build --app=shopping --title="โ€ฆ".

Integrations

SMSOnly through the SMSTrack bridge โ€” POST https://vhivesolutions.com/smstrack/api.php with action=send. Key in HS_SMSTRACK_API_KEY.
EmailOnly through /var/www/html/mailtrack/client/mail_client.php โ†’ sendTrackedEmail(). Key in HS_MAILTRACK_API_KEY.
AIAnthropic 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.
GoogleGIS JS-callback mode only. Client ID 974353825718-c3t4c2ilcn2t55gi7o61s27htcj3e8ae.apps.googleusercontent.com; vhivesolutions.com is an authorised origin.
ChartsChart.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 failed rows 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;