MolinoPro

INTEGRATION-CONTRACT

Master Codebase Guidebook
Markdown + HTML Dev-Docs Renderer - Frontend Client Module

Default Index
Open README.md
Root: README.mdtools
Milestones
H1Molino → Spaces Integration Contract
H2Architecture
Molino App (source of truth)          Spaces App (standalone)
├── Trips                              ├── Spaces DB
├── Experiences                        ├── SpaceSections DB
├── Offers                             ├── Profiles / Categories
├── Orders                             ├── Public rendering
├── Documents                          └── Discovery
└── Skills
            │
            │  POST /api/spaces  (Bearer token)
            ▼
    Spaces creates/returns spaceId, slug, publicUrl
            │
            ▼
    Molino stores ExternalSpace reference only

Rule: Spaces DB = source of truth for Spaces. Molino only stores external refs. No cross-DB writes.


H21. Spaces API Contract
H3Auth
Authorization: Bearer <SPACES_API_KEY>

Both apps share the same SPACES_API_KEY env var.

H3POST /api/spaces — Create space with sections

Request:

{
  "title": "Al-Andalus Experience",
  "slug": "al-andalus-experience",
  "path": "/al-andalus-experience",
  "status": "draft",
  "visibility": "public",
  "sections": [
    { "type": "poster", "content": { /* poster content */ } },
    { "type": "hero", "content": { /* hero content */ } }
  ]
}

Response:

{
  "success": true,
  "space": {
    "id": "clxxx...",
    "title": "Al-Andalus Experience",
    "slug": "al-andalus-experience",
    "status": "draft",
    "path": "/al-andalus-experience",
    "sections": [
      { "id": "sec...", "type": "poster", "order": 0, "enabled": true },
      { "id": "sec...", "type": "hero", "order": 1, "enabled": true }
    ]
  }
}

Required fields: title, slug, path Defaults: status: "draft", visibility: "public", sections: []


H22. Molino Prisma Model

Add to Molino's schema.prisma:

model ExternalSpace {
  id              String   @id @default(cuid())
  entityType      String
  entityId        String
  externalSpaceId String   @unique
  slug            String?
  publicUrl       String?
  status          String   @default("draft")
  provider        String   @default("spaces")
  createdAt       DateTime @default(now())
  updatedAt       DateTime @updatedAt

  @@index([entityType, entityId])
  @@index([provider])
}

H23. Molino Server Action
// app/(spaces)/actions/createExternalSpaceFromEntity.ts
"use server"

import { prisma } from "@/lib/prisma"

type EntityType = "trip" | "experience" | "offer" | "product" | "skill" | "studio_service" | "platform_service"

export async function createExternalSpaceFromEntity(input: {
  entityType: EntityType
  entityId: string
  payload: {
    title: string
    summary?: string
    categorySlug?: string
    visibility?: "private" | "unlisted" | "public" | "local_public"
    geo?: {
      mode: "none" | "city" | "region" | "country" | "global"
      city?: string
      region?: string
      country?: string
      lat?: number
      lon?: number
      radiusKm?: number
    }
    sections: Array<{ key: string; order: number; content: unknown }>
    cta?: { label: string; href: string }
  }
}) {
  const apiUrl = process.env.SPACES_API_URL
  const apiKey = process.env.SPACES_API_KEY
  if (!apiUrl || !apiKey) throw new Error("Spaces API not configured")

  const response = await fetch(`${apiUrl}/api/spaces`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      sourceApp: "molino",
      sourceEntityType: input.entityType,
      sourceEntityId: input.entityId,
      ...input.payload,
    }),
  })

  if (!response.ok) {
    const message = await response.text()
    throw new Error(`Spaces API failed: ${message}`)
  }

  const result = await response.json()

  await prisma.externalSpace.upsert({
    where: { externalSpaceId: result.space.id },
    create: {
      entityType: input.entityType,
      entityId: input.entityId,
      externalSpaceId: result.space.id,
      slug: result.space.slug,
      publicUrl: `${process.env.SPACES_PUBLIC_BASE_URL}/spaces/${result.space.slug}`,
      status: result.space.status,
    },
    update: {
      slug: result.space.slug,
      publicUrl: `${process.env.SPACES_PUBLIC_BASE_URL}/spaces/${result.space.slug}`,
      status: result.space.status,
    },
  })

  return result
}

H24. Spaces-side API (enhanced /api/spaces/route.ts)

The existing route already handles POST with sections. It needs minor updates to accept external source fields. Add these fields to the Space model in Spaces' Prisma:

model Space {
  // ... existing fields ...
  sourceApp        String?
  sourceEntityType String?
  sourceEntityId   String?
  categorySlug     String?
  geoMode          String   @default("none")
  city             String?
  region           String?
  country          String?
  latitude         Float?
  longitude        Float?
  radiusKm         Float?
}

The API already creates spaces with sections. The body shape it accepts:

{
  title: string,
  slug: string,
  path: string,
  status?: "draft" | "published" | "scheduled" | "archived" | "broken",
  visibility?: "private" | "unlisted" | "public",
  sourceApp?: string,
  sourceEntityType?: string,
  sourceEntityId?: string,
  categorySlug?: string,
  sections: Array<{ type: string; content: object }>
}

H25. Section Content JSON Schemas
H318 section types available in the registry. Each section object in the `sections` array:
{ "type": "<section_type>", "content": { /* see schemas below */ } }

H3`poster` — Event poster layout
{
  "type": "poster",
  "content": {
    "title": "RADIO CROONER",
    "subtitle": "GRXGRASS PRESENTS",
    "date": "5 MARZO",
    "time": "21:00 H",
    "location": "ENTRADA EXCLUSIVA PARA SOCIOS",
    "image": "https://...",
    "theme": "yellow",
    "badgeText": "MÚSICA EN VIVO"
  }
}
  • title (required) string
  • subtitle (optional) string
  • date (optional) string
  • time (optional) string
  • location (optional) string
  • image (optional) string — URL
  • theme (required) "yellow" | "blue" | "bw"
  • badgeText (optional) string

H3`hero` — Hero banner with CTA
{
  "type": "hero",
  "content": {
    "title": "Discover Andalusia",
    "subtitle": "7 days of authentic Spanish culture",
    "backgroundImage": "https://...",
    "ctaText": "Book Now",
    "ctaLink": "/book"
  }
}
  • title (required) string
  • subtitle (optional) string
  • backgroundImage (optional) string — URL or empty
  • ctaText (optional) string
  • ctaLink (optional) string

H3`rich_text` — Long-form content
{
  "type": "rich_text",
  "content": {
    "html": "<p>Join us for an unforgettable evening...</p>"
  }
}
  • html (required) string

H3`cta` — Call to action
{
  "type": "cta",
  "content": {
    "subtitle": "Ready to Get Started?",
    "content": "Invite the audience to take the next step.",
    "ctaText": "Join Now",
    "ctaLink": "/signup",
    "mediaUrl": "https://..."
  }
}
  • subtitle (optional) string
  • content (optional) string
  • ctaText (required) string
  • ctaLink (required) string
  • mediaUrl (optional) string — URL or empty

H3`cards` — Grid of cards
{
  "type": "cards",
  "content": {
    "items": [
      {
        "title": "Ceramic Vases",
        "description": "Hand-painted traditional designs",
        "image": "https://...",
        "link": "/shop/vases"
      }
    ]
  }
}
  • items (required) array of:
    • title (required) string
    • description (required) string
    • image (optional) string — URL or empty
    • link (optional) string

H3`slider_native` — Horizontal image slider
{
  "type": "slider_native",
  "content": {
    "items": [
      {
        "image": "https://...",
        "title": "Handmade Ceramics",
        "description": "Traditional techniques"
      }
    ]
  }
}
  • items (required) array of:
    • image (required) string — URL
    • title (optional) string
    • description (optional) string

H3`features` — Feature highlights grid
{
  "type": "features",
  "content": {
    "title": "Festival Highlights",
    "subtitle": "What awaits you",
    "items": [
      {
        "title": "Live Flamenco",
        "description": "Authentic performances by local artists"
      }
    ]
  }
}
  • title (optional) string
  • subtitle (optional) string
  • items (required) array of:
    • title (required) string
    • description (required) string

H3`testimonials` — Customer quotes
{
  "type": "testimonials",
  "content": {
    "title": "Past Attendees",
    "subtitle": "What people say",
    "content": "Testimonials increase trust.",
    "items": [
      {
        "quote": "The most authentic cultural experience in Spain.",
        "author": "Maria L.",
        "role": "Visitor",
        "avatar": "",
        "logo": ""
      }
    ]
  }
}
  • title (optional) string
  • subtitle (optional) string
  • content (optional) string
  • items (required) array of:
    • quote (required) string
    • author (required) string
    • role (required) string
    • avatar (optional) string — URL or empty
    • logo (optional) string

H3`pricing` — Tiered pricing plans
{
  "type": "pricing",
  "content": {
    "title": "Tour Packages",
    "subtitle": "Choose your adventure",
    "content": "",
    "plans": [
      {
        "name": "Essential",
        "price": "€349",
        "period": "per person",
        "description": "Core experience",
        "features": ["3-day tour", "Hotel included", "Expert guide"],
        "buttonText": "Book",
        "buttonLink": "/book/essential",
        "isPopular": false
      }
    ]
  }
}
  • title (optional) string
  • subtitle (optional) string
  • content (optional) string
  • plans (required) array of:
    • name (required) string
    • title (optional) string
    • price (required) string
    • period (required) string
    • description (required) string
    • features (required) string[]
    • buttonText (required) string
    • buttonLink (required) string
    • isPopular (optional) boolean

H3`faq` — Frequently asked questions
{
  "type": "faq",
  "content": {
    "title": "Common Questions",
    "subtitle": "Everything you need to know",
    "content": "",
    "faqs": [
      {
        "question": "What is included in the price?",
        "answer": "All tours include accommodation, guided visits, entry tickets."
      }
    ]
  }
}
  • subtitle (optional) string
  • content (optional) string
  • faqs (required) array of:
    • question (required) string
    • answer (required) string

H3`footer` — Page footer
{
  "type": "footer",
  "content": {
    "text": "© 2026 Heritage Festival. All rights reserved."
  }
}
  • text (required) string

H3`itinerary` — Day-by-day tour itinerary
{
  "type": "itinerary",
  "content": {
    "title": "Your Journey",
    "days": [
      {
        "day": "1",
        "title": "Arrival in Seville",
        "activities": ["Airport pickup", "Welcome dinner", "Evening walk"]
      }
    ]
  }
}
  • title (optional) string
  • days (required) array of:
    • day (required) string
    • title (required) string
    • activities (required) string[]

H3`mission_statement` — Mission/vision statement
{
  "type": "mission_statement",
  "content": {
    "title": "Our Approach",
    "content": "We combine deep industry expertise with innovative methodologies..."
  }
}
  • title (optional) string
  • content (required) string

H3`feature_grid` — Grid of features with icons
{
  "type": "feature_grid",
  "content": {
    "title": "Core Competencies",
    "items": [
      {
        "title": "Strategy",
        "description": "Market analysis & planning",
        "icon": "chart"
      }
    ]
  }
}
  • title (optional) string
  • items (required) array of:
    • title (required) string
    • description (required) string
    • icon (optional) string

H3`tour_cards` — Tour package cards
{
  "type": "tour_cards",
  "content": {
    "items": [
      {
        "title": "City Explorer",
        "description": "Seville & Córdoba",
        "image": "https://...",
        "duration": "3 days",
        "price": "€349"
      }
    ]
  }
}
  • items (required) array of:
    • title (required) string
    • description (required) string
    • image (optional) string — URL or empty
    • duration (optional) string
    • price (optional) string

H3`destination_grid` — Destination image grid
{
  "type": "destination_grid",
  "content": {
    "title": "Destinations",
    "items": [
      {
        "name": "Seville",
        "image": "https://...",
        "description": "Capital of Andalusia"
      }
    ]
  }
}
  • title (optional) string
  • items (required) array of:
    • name (required) string
    • image (required) string — URL
    • description (optional) string

H3`slider_html` — Image slider with style variants
{
  "type": "slider_html",
  "content": {
    "slides": [
      {
        "image": "https://...",
        "title": "Slide 1",
        "description": "First slide description",
        "link": "/book",
        "linkText": "Learn More"
      }
    ],
    "style": "hero",
    "autoPlay": true,
    "interval": 5000
  }
}
  • slides (required) array of:
    • image (required) string
    • title (optional) string
    • description (optional) string
    • link (optional) string
    • linkText (optional) string
  • style (optional) "hero" | "poster" | "minimal"
  • autoPlay (optional) boolean — default true
  • interval (optional) number — ms, default 5000

H26. Env Vars

Molino:

SPACES_API_URL=https://spaces-next-app-3kui34kvnq-ew.a.run.app
SPACES_API_KEY=<shared-secret>
SPACES_PUBLIC_BASE_URL=https://spaces-next-app-3kui34kvnq-ew.a.run.app

Spaces:

SPACES_API_KEY=<same-shared-secret>

H27. Implementation Order
  1. Spaces: Verify /api/spaces POST route works with Bearer token auth (already exists)
  2. Spaces: Add geo + source fields to Space model (optional, for Layer 4)
  3. Molino: Add ExternalSpace model to Prisma schema
  4. Molino: Add createExternalSpaceFromEntity server action
  5. Molino: Build entity→space section mappers (computeTripSpaceSections, etc.)
  6. Molino: Add "Publish to Spaces" buttons to admin pages
  7. Both: Test end-to-end with one trip entity

H28. Quick Seed Payload Example

Complete POST body to create a fully populated space:

{
  "title": "Al-Andalus Experience",
  "slug": "al-andalus-experience",
  "path": "/al-andalus-experience",
  "status": "published",
  "visibility": "public",
  "sections": [
    {
      "type": "poster",
      "content": {
        "title": "AL-ANDALUS",
        "subtitle": "EXPERIENCE",
        "date": "2026",
        "time": "GRANADA",
        "location": "AUTHENTIC SPANISH CULTURE",
        "image": "https://picsum.photos/seed/alandalus/800/600",
        "theme": "yellow",
        "badgeText": "EXCLUSIVE"
      }
    },
    {
      "type": "mission_statement",
      "content": {
        "title": "Our Mission",
        "content": "To deliver authentic Andalusian experiences through immersive cultural journeys."
      }
    },
    {
      "type": "hero",
      "content": {
        "title": "Discover Andalusia",
        "subtitle": "7 days of authentic Spanish culture, history and cuisine",
        "backgroundImage": "https://picsum.photos/seed/andalusia-hero/800/600",
        "ctaText": "Book Now",
        "ctaLink": "/book"
      }
    },
    {
      "type": "itinerary",
      "content": {
        "title": "Your Journey",
        "days": [
          { "day": "1", "title": "Arrival in Seville", "activities": ["Airport pickup", "Welcome dinner"] },
          { "day": "2", "title": "Seville Explorer", "activities": ["Real Alcázar", "Cathedral tour", "Tapas crawl"] },
          { "day": "3", "title": "Córdoba Day", "activities": ["Great Mosque", "Jewish Quarter", "Flamenco show"] }
        ]
      }
    },
    {
      "type": "destination_grid",
      "content": {
        "title": "Destinations",
        "items": [
          { "name": "Seville", "image": "https://picsum.photos/seed/seville/400/300", "description": "Capital of Andalusia" },
          { "name": "Córdoba", "image": "https://picsum.photos/seed/cordoba/400/300", "description": "Great Mosque & patios" },
          { "name": "Granada", "image": "https://picsum.photos/seed/granada/400/300", "description": "Alhambra Palace" }
        ]
      }
    },
    {
      "type": "tour_cards",
      "content": {
        "items": [
          { "title": "City Explorer", "description": "Seville & Córdoba", "image": "https://picsum.photos/seed/city-tour/400/300", "duration": "3 days", "price": "€349" },
          { "title": "Complete Tour", "description": "All highlights included", "image": "https://picsum.photos/seed/complete/400/300", "duration": "7 days", "price": "€899" }
        ]
      }
    },
    {
      "type": "pricing",
      "content": {
        "title": "Tour Packages",
        "subtitle": "Choose your adventure",
        "plans": [
          { "name": "Essential", "price": "€349", "period": "per person", "description": "Core experience", "features": ["3-day tour", "Hotel included", "Expert guide"], "buttonText": "Book", "buttonLink": "/book/essential" },
          { "name": "Complete", "price": "€899", "period": "per person", "description": "Full immersion", "features": ["7-day tour", "Boutique hotels", "All meals", "Private transport"], "buttonText": "Book", "buttonLink": "/book/complete", "isPopular": true }
        ]
      }
    },
    {
      "type": "faq",
      "content": {
        "subtitle": "Common Questions",
        "faqs": [
          { "question": "What is included?", "answer": "All tours include accommodation, guided visits, and entry tickets." },
          { "question": "Is this suitable for families?", "answer": "Yes! Children under 12 receive a 30% discount." }
        ]
      }
    },
    {
      "type": "cta",
      "content": {
        "subtitle": "Ready for Adventure?",
        "content": "Book your Andalusian journey today.",
        "ctaText": "Reserve Now",
        "ctaLink": "/book"
      }
    },
    {
      "type": "footer",
      "content": {
        "text": "© 2026 Al-Andalus Experience. All rights reserved."
      }
    }
  ]
}