Voxel Guide — Product & Technical Specification
Target stack: Laravel 13 · PHP 8.3+ · Livewire 3 · Alpine · Tailwind 4 · PostgreSQL 16 · Redis · Meilisearch · Stripe Connect
Replaces: WordPress + Voxel theme + Elementor (voxel.guide) and its successor build (stagingvoxel.wp1.site)
Document status: v1.0 — derived from live audit of both sites, July 2026
1. What Voxel Guide is
Voxel Guide is an independent community hub and multi-vendor marketplace for the Voxel WordPress theme ecosystem. It does three jobs at once, and the rebuild must keep all three first-class:
- A discovery library. Curated showcases of sites built with Voxel, plus code snippets, automations, and how-to content. This is the SEO engine and the top of the funnel.
- A marketplace. Independent developers sell addons and assets; the platform takes a fee. Today this is Voxel's built-in product fields; the rebuild makes it a real Stripe Connect marketplace with licensing.
- A talent network. Developer profiles, ratings, a job board, and a quote/lead flow.
The site carries an explicit disclaimer — "We have no affiliation with the Voxel team but this site was made with their permission." — which must be preserved verbatim in the footer.
1.1 Audit summary of the current system
Both sites run WordPress with the Voxel theme, Elementor, and ~30–40 plugins. Live content counts pulled from the production sitemap:
| Content type | Prod URL prefix | Prod count | On staging |
|---|---|---|---|
| Website showcase | /website/{slug} |
120 | yes |
| Snippet | /snippet/{slug} |
114 | yes |
| Asset | /asset/{slug} |
102 | yes |
| Article (Tips/How-to/FAQ) | /article/{slug} |
19 | yes |
| Job | /job/{slug} |
1 | yes |
| Page | /{slug} |
47 | yes |
| Post | /{slug} |
2 | yes |
| Addon | /addon/{slug} |
— | staging only |
| Deal | /deal/{slug} |
— | staging only |
| Workflow | /workflow/{slug} |
— | staging only |
| Meetup | /meetup/{slug} |
— | staging only |
| Profile | /profile/{username} |
— | staging only |
Read this carefully: staging is not a superset of production by content, only by schema. Staging
introduced five content types that production never had, while production kept accumulating real
submissions (its website sitemap has 120 entries vs. staging's ~103). The migration therefore has to
merge two diverging datasets, not simply promote staging. See MIGRATION.md §3.
Notable plugin dependencies that encode business logic (each needs an explicit disposition):
Premium Quote Request System - Multi Vendor— powers "Add to Quote" (staging only)Claim Posts— lets a vendor claim a listing someone else submittedCPT Completeness Score— listing quality scoringVoxel Toolkit,{CODICTS} Voxel Bulk Messages,Voxel Twilio Phone VerificationFluentCommunity(+Pro),Fluent Support(+Pro),FluentCRM(+Pro) — staying on WordPress, see §9SEOPress(+Pro) — sitemaps and meta; the rebuild owns this nativelyFathom Analytics,AnalyticsWP,ShortPixel,CleanTalk,accessiBe
2. Roles and permissions
| Role | Can |
|---|---|
| Guest | Browse everything except gated outbound links; see prices; cannot see live site URLs on showcases (current behaviour: "Login to View Website Link") |
| Member | Submit showcases/snippets/workflows, save & collect, vote, comment, review purchases, message, build quotes, check in to meetups |
| Vendor | Everything a Member can, plus: sell assets/addons, manage releases & license keys, view sales/payout dashboards, respond to reviews. Requires completed Stripe Connect onboarding |
| Employer | Post and manage jobs, review applicants. Any Member can become one by posting |
| Moderator | Approve/reject submissions, verify showcases, handle reports, edit any listing, merge duplicates |
| Admin | All of the above, plus fee configuration, payouts, refunds, feature flags, user administration |
Implemented as a roles/permissions pair (spatie/laravel-permission) with policy classes per model.
Vendor is a capability flag earned via Stripe onboarding, not merely a role assignment — gate on
$user->vendor?->payouts_enabled.
3. Domain model
3.1 Shared concerns
Nearly every content type shares the same behaviours. Model these once as traits + polymorphic tables rather than duplicating columns eleven times:
Listingbase behaviour:slug,title,excerpt,body,status,published_at,user_id,views,search_vector, soft deletesHasVotes→votes(morph, unique on user+votable)HasSaves→saves(morph) andcollections/collection_itemsHasComments→comments(morph, threaded, 1 level of nesting)HasMedia→media(morph, ordered, S3/R2-backed, responsive derivatives)HasTaxonomies→taxonomy_terms+taxonomables(morph pivot)Reportable→reports(morph) feeding the moderation queueAuditable→activity_log
Status lifecycle (every submittable type): draft → pending → published → archived, plus
rejected and spam. Transitions emit events; pending → published is the moderation gate.
3.2 Content types
websites — showcase
The flagship SEO asset. Fields observed live: gallery images, description, live URL, category, plugins used, page builder, view count, verified badge with timestamp, submitter, save, vote, add-to-quote.
websites
id, slug, title, description, live_url, screenshot_id
user_id (submitter)
claimed_by_id (nullable — the "Claim Posts" behaviour)
verified_at, verified_by_id
views, votes_count, status, published_at
Taxonomies: website_type (Business/Corporate, E-Commerce, Directory/Contact, Community Forum,
Booking, Non-Profit, Blog, Event, Marketplace), plugins_used, page_builder.
live_url is gated: guests see a "Sign in to view" affordance. Preserve this — it drives signups.
Store outbound clicks in link_clicks for vendor analytics.
snippets
snippets
id, slug, title, instructions (markdown), screenshot_id
user_id, views, votes_count, status, published_at
snippet_blocks -- a snippet has MANY code blocks; drives the "+1 / +2" card badges
id, snippet_id, language (php|js|css|html|sql|json), label, code, position
Server-side syntax highlighting at save time (Shiki via a queued job, cached as HTML) so pages ship zero highlighting JS. Copy-to-clipboard is Alpine, per block and for all blocks.
assets and addons — the sellable types
These share a product concern. An asset is a downloadable file (template, JSON export, config); an
addon is a distributable plugin with versions and licensing. Both may be free or paid.
products -- polymorphic parent for asset/addon
id, productable_type, productable_id
vendor_id, price_cents, currency, is_free
license_type (single|extended|unlimited), activation_limit
external_url -- some addons sell off-site; keep as a link-out product
sales_count, rating_avg, rating_count
product_versions
id, product_id, version, changelog, file_path, file_size, min_voxel_version, released_at
product_files -- for multi-file assets
license_keys
id, product_id, order_item_id, user_id, key, activation_limit, status, expires_at
license_activations
id, license_key_id, site_url, activated_at, deactivated_at
Taxonomies: template_type (assets), addon_category, compatibility (Voxel version ranges).
Design note: production sells addons that live on the vendor's own site (external_url). Don't force
those into checkout — model them as listing-only products with an outbound CTA, and let vendors upgrade
to on-platform selling later. This is the difference between migrating 100% of the catalogue and
migrating 40% of it.
jobs
jobs
id, slug, title, description, employer_id
project_type (one_time|recurring)
experience_level (beginner|intermediate|advanced)
payment_type (fixed|hourly)
budget_min_cents, budget_max_cents, currency
location, is_remote, applications_count
status, published_at, expires_at
job_applications
id, job_id, user_id, cover_letter, proposed_rate_cents, status
profiles — vendor / developer
Observed fields: avatar, local time, overall rating plus three sub-scores, about, skills, hourly rate, location, member-since, for-hire flag, per-type portfolio counts, social links, message, check-in.
profiles
id, user_id, display_name, headline, bio, avatar_id, cover_id
location, timezone, hourly_rate_cents, is_for_hire
website, social (jsonb: facebook, youtube, twitter, linkedin, instagram)
rating_avg, rating_communication, rating_accuracy, rating_value, rating_count
profile_skills -- pivot to a curated `skills` table (CSS, HTML, PHP, JS, AI, APIs,
-- Payment Gateways, Plugin Creation, Dynamic Conditions, Site Speed, Image Optimization…)
Portfolio counts are derived, cached counters, not stored truth — recompute on listing publish.
deals, workflows, meetups, articles
Lighter types, all built on the Listing base.
deals— partner offers:vendor_name,discount_label,coupon_code,affiliate_url,starts_at,ends_atworkflows— automation recipes:platform(FlowMattic/OttoKit/Make/n8n/Zapier),json_export,difficulty,required_pluginsmeetups— events:starts_at,timezone,location/is_online,join_url,recording_url,attendees(check-in pivot, mirroring the current "Check in" behaviour)articles— Tips n Tricks, How-To-Videos, FAQs, Complete Walkthrough. One model, onearticle_typeenum, four archive routes
3.3 Commerce
orders id, user_id, number, subtotal_cents, fee_cents, tax_cents, total_cents,
currency, status, stripe_payment_intent_id, placed_at
order_items id, order_id, product_id, vendor_id, unit_price_cents, platform_fee_cents,
vendor_amount_cents, stripe_transfer_id
vendors id, user_id, stripe_account_id, payouts_enabled, charges_enabled,
fee_percent (nullable override), payout_schedule
payouts id, vendor_id, amount_cents, period_start, period_end, stripe_payout_id, status
refunds id, order_item_id, amount_cents, reason, stripe_refund_id, status
reviews id, product_id, user_id, order_item_id, rating, body, vendor_reply, published_at
quotes id, user_id, session_id, status -- the "Add to Quote" basket
quote_items id, quote_id, quotable_type, quotable_id, note
Reviews require a matching order_item_id (or a free download record) — verified purchases only.
3.4 Cross-cutting
users, sessions, notifications, saves, collections, votes, comments, reports,
media, taxonomy_terms, taxonomables, messages, conversations, link_clicks,
redirects (legacy URL → new URL), activity_log
4. Information architecture & routes
/ home — discovery-led
/websites showcase archive (filters: type, plugins, page builder)
/websites/{slug}
/snippets /snippets/{slug}
/assets /assets/{slug}
/addons /addons/{slug}
/jobs /jobs/{slug} /jobs/post
/workflows /workflows/{slug}
/deals /deals/{slug}
/meetups /meetups/{slug}
/hire developer directory (filters: skills, rate, availability, rating)
/@{username} public profile — short, memorable, replaces /profile/{u}
/learn/tips /learn/videos /learn/faqs /learn/walkthrough
/search unified search results
/submit/{type} submission wizard
/cart /checkout /orders/{number}
/dashboard member: saves, collections, submissions, orders, licenses, messages
/dashboard/vendor vendor: products, releases, sales, payouts, reviews
/admin moderation queue, users, fees, reports
/fees fee calculator (marketing tool — keep)
Slug change: singular /website/x → plural /websites/x aligns every type and reads better, but it
is a 500+ URL redirect map. It is worth doing once, at migration, never later. All legacy paths are
handled by the redirects table (§7.3).
/profile/{u} → /@{u} likewise, with permanent redirects.
5. Key user flows
5.1 Submit a showcase
Multi-step Livewire wizard, autosaving to draft on every step:
- URL → server fetches title, favicon, OG image, and captures a screenshot (queued, Browsershot/ Playwright) so the user isn't asked to upload one
- Category + plugins + page builder (typeahead against existing terms; new terms need moderation)
- Description + extra gallery images
- Review → submit →
pending
Moderator sees it in /admin/queue. Approve → published + notification + optional social post.
5.2 Buy an asset
- Add to cart (guest cart in session, merged on login)
- Checkout — Stripe Payment Element; tax via Stripe Tax
PaymentIntentsucceeds →OrderPlacedjob:- split each item:
platform_fee_cents = round(price * fee_percent)via Stripetransfer_data - issue
license_keyper licensed item - grant download entitlement
- notify buyer + each vendor
- split each item:
- Buyer downloads from
/dashboard/licenses— signed, expiring URLs, activation-limited
5.3 Become a vendor
/dashboard/vendor/onboard → create Stripe Connect Express account → hosted onboarding →
webhook account.updated flips charges_enabled/payouts_enabled → product creation unlocks.
Until then, vendors may draft products but not publish paid ones.
5.4 Hire a developer
Browse /hire → filter → profile → Message (in-app conversation) or Request a quote (attaches
current quote basket). Quote requests notify by email + in-app, and are tracked to completion so ratings
can be requested afterwards.
6. Search
Global ⌘K palette is a signature of the current site and must survive. Back it with Meilisearch via Laravel Scout:
- One index per type + one federated
allindex for the palette - Indexed: title, excerpt, body (stripped), taxonomy terms, vendor/author name, skills
- Filterable: type, status, price band, is_free, taxonomy IDs, rating, verified
- Sortable: published_at, views, votes, sales, rating
- Typo tolerance on, prefix search on, 50ms budget
Archive filtering uses the same index so filter + search never disagree. Postgres FTS is the documented fallback if a managed Meilisearch is not desired — same Scout interface, one env var.
7. Non-functional requirements
7.1 Performance
- LCP < 1.8s on 4G for archive pages; server-render everything, no client-side data fetching on first paint
- Images: R2/S3 + on-the-fly resizing, AVIF/WebP with fallback, explicit
width/height,loading="lazy"below fold - Cache archive facet counts in Redis (5 min TTL); cache rendered snippet highlighting indefinitely
- Queue everything slow: screenshots, thumbnails, emails, search indexing, webhook processing (Horizon)
7.2 Accessibility
The current site pays for accessiBe, an overlay. Do not carry that forward — overlays are widely
regarded as ineffective and have drawn litigation. Build to WCAG 2.2 AA natively: semantic landmarks,
visible focus rings, keyboard-operable filter and palette, prefers-reduced-motion honoured, 4.5:1
contrast minimum. This is a cost saving and a quality improvement simultaneously.
7.3 SEO — the highest-risk area of this project
The site's value is 500+ indexed pages. Losing them loses the business.
redirectstable, seeded from a full crawl of both sitemaps before launch, served by middleware with 301s. Every legacy URL must resolve.- Native sitemap generation per type,
lastmodaccurate, submitted on deploy - Schema.org:
SoftwareApplication(addons/assets),JobPosting(jobs),Person(profiles),Event(meetups),Offer+AggregateRatingon products,BreadcrumbListeverywhere - Canonicals, OG/Twitter cards, per-type OG image templates rendered server-side
- Preserve
<title>/meta from SEOPress where a human wrote them (they migrate asmeta_title/meta_description)
7.4 Security
- Stripe webhooks signature-verified, idempotent by event ID
- Signed, short-lived download URLs; never expose S3 paths
- Rate limits on submission, messaging, voting, search
- Uploaded assets scanned (ClamAV) and served from a cookieless domain
- Snippet code is displayed, never executed; render as escaped text, strict CSP with nonces
- 2FA available for vendors; required for admins
8. Design system
Driven by the existing brand mark: teal #2D7492 and ink #282530, both lifted from the staging logo.
Every colour, radius, and font in the build is a CSS custom property under a single :root block, so a
rebrand is one file. The token contract is identical across all three design directions — that is what
makes them swappable:
--brand --brand-ink --brand-soft --brand-strong
--ink --ink-2 --ink-3
--surface --surface-2 --surface-3
--line --line-strong
--accent --ok --warn --danger
--radius --radius-lg --shadow-1 --shadow-2
--font-display --font-body --font-mono
Tailwind 4 consumes these via @theme, so utilities and custom CSS stay in sync. Dark mode is a second
:root[data-theme="dark"] block — no utility duplication.
The logo ships as inline SVG with fill="var(--brand)" and fill="var(--ink)", so it recolours with the
theme instead of needing per-theme PNG exports.
9. Scope boundary — what stays on WordPress
Per decision, this rebuild covers content + marketplace only. These stay where they are and are integrated, not replaced:
| System | Disposition |
|---|---|
| FluentCommunity | Stays. Integrate via SSO so voxel.guide and the community share one identity |
| Fluent Support | Stays. Deep-link from /dashboard; pass user context on handoff |
| FluentCRM | Stays as the ESP. Laravel emits events (signup, purchase, submission) to FluentCRM over its REST API |
Required to make this work: a single sign-on bridge. Laravel becomes the identity provider; the WordPress install authenticates against it via a signed-JWT plugin. Without this the user experience splits in two and the whole thing feels broken. Budget it as real work — it is roughly a two-week task, not an afternoon.
10. Build phases
| Phase | Contents | Est. |
|---|---|---|
| 0 | Repo, CI, staging infra, design system, auth, roles, media pipeline | 2 wk |
| 1 | Listing base, websites + snippets + articles, archives, search, submissions, moderation | 4 wk |
| 2 | Profiles, /hire, messaging, saves/collections/votes |
3 wk |
| 3 | Marketplace: products, Connect onboarding, cart, checkout, licensing, vendor dashboard | 5 wk |
| 4 | Jobs, deals, workflows, meetups, quote basket | 3 wk |
| 5 | SSO bridge to WordPress, CRM eventing, analytics | 2 wk |
| 6 | Migration dry runs, redirect map, SEO parity checks, launch | 3 wk |
~22 weeks of focused work. Phases 1–2 are independently shippable behind a beta flag.
11. Open questions
- Addon catalogue economics. Most addons currently link out to vendor sites. What share do you expect to convert to on-platform selling? If it is low, the licensing system is over-built for v1 and should move to Phase 5.
- Existing passwords. WordPress uses phpass. Laravel can verify these on first login and rehash
transparently — but only if the
wp_usershashes come across. Confirm DB access. Otherwise every user needs a reset email, which will cost some percentage of the userbase. - Which dataset wins. Production has more real content; staging has more schema. §3 of
MIGRATION.mdproposes prod-as-truth. Confirm. - Merch store. Currently empty. Drop it, or keep a print-on-demand link-out?
- Fee percentage. 15% is assumed throughout. Confirm before the payout maths is written.