LŌKAHI – A Marketplace Connecting Seekers with Holistic Wellness Practitioners

Hi everyone,

Excited to share my girlfriend’s and my latest project, built entirely with Anvil: LŌKAHI is a marketplace that connects people seeking holistic wellness support with the practitioners who can guide them.

Think of it as a home for holistic practitioners: discovery, messaging, bookings, and payments all in one place.

Normally I try to make MVPs when launching something with Anvil, but this time I went all in.

How it works:

  • Seekers search practitioners by modality, location, and intention.
  • They explore detailed profiles with offerings, approach, and availability.
  • Built-in messaging lets both sides check the fit before committing.
  • Sessions are booked and paid directly through the platform (Stripe), so practitioners never chase invoices.

The Anvil side, for the builders here:

  • Latest M3 Theme + new Routing dependency: the whole app runs on M3 design tokens with clean URLs like /practitioner/:slug.
  • Custom Layouts: A root layout carries the sticky glass header and footer that every public page slots into, and there are dedicated sidebar layouts for the seeker account area and the practitioner dashboard.
  • Dynamic social sharing images: Share a profile on WhatsApp and the preview shows that practitioner’s actual photo and bio (see this post and end of this post)
  • Custom passwordless login: my own take on magic links. No passwords, just a 5-digit code sent by email, bcrypt-hashed in a Data Table with expiry, rate limiting, and an attempt lockout. Signup and login are the same flow, so there is no separate registration step.
  • Custom spinners and skeleton loading: no default Anvil spinners anywhere. Pages and cards render instantly as shimmering placeholders and fade in when the data arrives, images included.
  • Consent-gated tracking: a cookie banner controls everything. Analytics only loads after the visitor opts in
  • AirDatepicker as an Anvil component: the JS library wrapped once in a custom component, so any form gets a slick inline calendar with min/max dates and a normal Anvil change event.
  • In-browser photo cropping: practitioners crop their profile and gallery photos right in the upload dialog (Cropper.js wrapped as an Anvil component), so every image lands perfectly framed.
  • Branded transactional emails: booking confirmations, login codes, and reminders all share one HTML layout sent through anvil.email, so every email looks like the app.
  • Live demo mode: prospective practitioners can click around a fully working practice dashboard (bookings, messages, earnings) without creating an account.
  • Location search via Photon: address autocomplete in the search bar and onboarding, powered by the free OpenStreetMap geocoder. No Google Maps API key, no billing account.
  • Stripe integration using the API for checkout and payouts.

If you’re a holistic practitioner (or know one), I’d love for you to give it a try.

Other feedback is always welcome!

3 Likes

This looks really clean!

1 Like

Thanks man! Definitely some credits to you for the skeleton loading of elements.

I also implemented this for images, where the skeleton stays visible until the image is fully loaded by the browser, not just when the component is rendered on the page.

CSS (theme.css):

@keyframes skeleton-loading {
  from { background: rgba(155,128,80,0.1); }
  to   { background: rgba(155,128,80,0.28); }
}
@keyframes skeleton-reveal {
  from { opacity: 0; } to { opacity: 1; }
}

.anvil-role-image-skeleton {
  animation: skeleton-loading 3s ease-in-out infinite alternate;
}
.anvil-role-image-skeleton img {
  opacity: 0 !important;
}
.anvil-role-image-revealed img {
  animation: skeleton-reveal 0.4s ease-out both;
}

Python helper:

from anvil.js.window import get_dom_node

def load_image(image_component, url):
  image_component.role = 'image-skeleton'
  dom = get_dom_node(image_component)
  img_el = dom.querySelector('img')

  def _reveal(e=None):
    img_el.onload = None
    img_el.onerror = None
    image_component.role = 'image-revealed'

  img_el.onload = _reveal
  img_el.onerror = _reveal
  image_component.source = url

  # Already cached? Reveal immediately.
  if img_el.complete and img_el.naturalWidth > 0:
    _reveal()
1 Like