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) stringsubtitle(optional) stringdate(optional) stringtime(optional) stringlocation(optional) stringimage(optional) string — URLtheme(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) stringsubtitle(optional) stringbackgroundImage(optional) string — URL or emptyctaText(optional) stringctaLink(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) stringcontent(optional) stringctaText(required) stringctaLink(required) stringmediaUrl(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) stringdescription(required) stringimage(optional) string — URL or emptylink(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 — URLtitle(optional) stringdescription(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) stringsubtitle(optional) stringitems(required) array of:title(required) stringdescription(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) stringsubtitle(optional) stringcontent(optional) stringitems(required) array of:quote(required) stringauthor(required) stringrole(required) stringavatar(optional) string — URL or emptylogo(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) stringsubtitle(optional) stringcontent(optional) stringplans(required) array of:name(required) stringtitle(optional) stringprice(required) stringperiod(required) stringdescription(required) stringfeatures(required) string[]buttonText(required) stringbuttonLink(required) stringisPopular(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) stringcontent(optional) stringfaqs(required) array of:question(required) stringanswer(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) stringdays(required) array of:day(required) stringtitle(required) stringactivities(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) stringcontent(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) stringitems(required) array of:title(required) stringdescription(required) stringicon(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) stringdescription(required) stringimage(optional) string — URL or emptyduration(optional) stringprice(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) stringitems(required) array of:name(required) stringimage(required) string — URLdescription(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) stringtitle(optional) stringdescription(optional) stringlink(optional) stringlinkText(optional) string
style(optional)"hero" | "poster" | "minimal"autoPlay(optional) boolean — default trueinterval(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
- Spaces: Verify
/api/spacesPOST route works with Bearer token auth (already exists) - Spaces: Add geo + source fields to Space model (optional, for Layer 4)
- Molino: Add
ExternalSpacemodel to Prisma schema - Molino: Add
createExternalSpaceFromEntityserver action - Molino: Build entity→space section mappers (
computeTripSpaceSections, etc.) - Molino: Add "Publish to Spaces" buttons to admin pages
- 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."
}
}
]
}