Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
727f288
feat: add health vertical tracer
SourceSenseiTheRealOne Aug 20, 2026
e154f44
feat: add localized Clerk identity foundation
SourceSenseiTheRealOne Aug 21, 2026
a52ec15
chore: stack Clerk identity foundation for user mapping
SourceSenseiTheRealOne Aug 21, 2026
d0cf80c
feat: add durable internal user mapping
SourceSenseiTheRealOne Aug 21, 2026
bf1b7e3
docs: define Juntly MVP vertical slices
SourceSenseiTheRealOne Aug 23, 2026
80992c9
docs: plan account capability onboarding
SourceSenseiTheRealOne Aug 23, 2026
82120c4
feat: add user account capability schema
SourceSenseiTheRealOne Aug 23, 2026
ed29628
feat: add account capability service
SourceSenseiTheRealOne Aug 23, 2026
fe62951
feat: add protected account capability API
SourceSenseiTheRealOne Aug 23, 2026
96e0233
feat: add same-origin account capability BFF
SourceSenseiTheRealOne Aug 23, 2026
d2d16eb
feat: add account capability onboarding UI
SourceSenseiTheRealOne Aug 23, 2026
3508009
fix: bound local Clerk clock skew tolerance
SourceSenseiTheRealOne Aug 23, 2026
8743316
docs: define taxonomy and provider profile slice
SourceSenseiTheRealOne Aug 23, 2026
d72ba9d
docs: plan taxonomy and provider profiles
SourceSenseiTheRealOne Aug 23, 2026
eb5a076
chore: verify launch reference data
SourceSenseiTheRealOne Aug 23, 2026
9b0818c
feat: add provider marketplace reference schema
SourceSenseiTheRealOne Aug 23, 2026
8938738
feat: add marketplace reference catalog
SourceSenseiTheRealOne Aug 23, 2026
e1d0e12
feat: add owner-only provider profiles
SourceSenseiTheRealOne Aug 23, 2026
bfd257e
feat: add provider reference and profile API
SourceSenseiTheRealOne Aug 23, 2026
9d23caa
feat: add provider profile BFF
SourceSenseiTheRealOne Aug 23, 2026
ea07305
feat: add provider profile onboarding
SourceSenseiTheRealOne Aug 23, 2026
d0f7317
fix: accept deterministic reference UUIDs
SourceSenseiTheRealOne Aug 23, 2026
e53ed3e
docs: define listings and moderation slice
SourceSenseiTheRealOne Aug 24, 2026
625b6bd
feat: add listing moderation persistence
SourceSenseiTheRealOne Aug 24, 2026
d9cc927
feat: add provider listing drafts
SourceSenseiTheRealOne Aug 24, 2026
3b16f82
feat: add listing moderation lifecycle
SourceSenseiTheRealOne Aug 24, 2026
aaae582
feat: add safe listing media boundary
SourceSenseiTheRealOne Aug 24, 2026
4b23231
feat: add listing moderation API
SourceSenseiTheRealOne Aug 24, 2026
a35a255
feat: add listing moderation BFF
SourceSenseiTheRealOne Aug 24, 2026
2c91eea
feat: add listing moderation UI
SourceSenseiTheRealOne Aug 24, 2026
6b8b5d9
fix: complete listing lifecycle acceptance
SourceSenseiTheRealOne Aug 24, 2026
05110cd
docs: define public discovery slice
SourceSenseiTheRealOne Aug 24, 2026
1bf6664
feat: add public listing discovery
SourceSenseiTheRealOne Aug 24, 2026
43cd042
feat: add public discovery API
SourceSenseiTheRealOne Aug 24, 2026
08cb673
feat: add public discovery pages
SourceSenseiTheRealOne Aug 24, 2026
caf902e
docs: define contact reveal slice
SourceSenseiTheRealOne Aug 24, 2026
79c5824
feat: add encrypted contact reveal vault
SourceSenseiTheRealOne Aug 24, 2026
a1c94de
feat: guard contact reveal decryption
SourceSenseiTheRealOne Aug 24, 2026
744f0fb
feat: add durable contact reveal limits
SourceSenseiTheRealOne Aug 24, 2026
79167f9
feat: add provider contact channel service
SourceSenseiTheRealOne Aug 24, 2026
6233b97
feat: persist encrypted contact channels
SourceSenseiTheRealOne Aug 24, 2026
bb2adf6
feat: add contact channel status reads
SourceSenseiTheRealOne Aug 24, 2026
5cd46b6
feat: add protected contact reveal transport
SourceSenseiTheRealOne Aug 24, 2026
10aab99
feat: add private contact reveal UI
SourceSenseiTheRealOne Aug 24, 2026
56961c8
fix: guide contact channel format
SourceSenseiTheRealOne Aug 24, 2026
d2ee00f
docs: record contact reveal proof matrix
SourceSenseiTheRealOne Aug 24, 2026
faa3e06
docs: finalize contact reveal acceptance
SourceSenseiTheRealOne Aug 24, 2026
cc47570
feat: redesign marketplace experience
SourceSenseiTheRealOne Sep 1, 2026
908b292
feat: add private messaging and notifications
SourceSenseiTheRealOne Sep 1, 2026
2227935
feat: add quotation requests and private proposals
SourceSenseiTheRealOne Sep 1, 2026
5b75955
feat: add booking state machine
SourceSenseiTheRealOne Sep 1, 2026
dbccd61
feat: add verified reviews and reputation
SourceSenseiTheRealOne Sep 1, 2026
1b03ee9
feat: add plans subscriptions and promotions
SourceSenseiTheRealOne Sep 1, 2026
c9bb431
feat: add audited administration and analytics
SourceSenseiTheRealOne Sep 1, 2026
62ff12e
feat: harden launch operations and deployment
SourceSenseiTheRealOne Sep 1, 2026
dfab222
fix: remediate dependency advisories
SourceSenseiTheRealOne Sep 1, 2026
2c75a76
chore: restore frontend quality gates
SourceSenseiTheRealOne Sep 1, 2026
6a8910c
fix: align deployment encryption configuration
SourceSenseiTheRealOne Sep 1, 2026
48daf9f
feat: publish immutable release images
SourceSenseiTheRealOne Sep 1, 2026
f6beb7a
feat: modernize marketplace design system
SourceSenseiTheRealOne Sep 2, 2026
eccd054
feat: complete marketplace homepage
SourceSenseiTheRealOne Sep 2, 2026
96bf0bc
fix: complete marketplace usability flows
SourceSenseiTheRealOne Sep 2, 2026
53ef6f6
fix: compact marketplace controls
SourceSenseiTheRealOne Sep 2, 2026
fcc2bd9
feat: rebrand marketplace UI to Vila
SourceSenseiTheRealOne Sep 2, 2026
bb56535
feat: add sole-admin moderation and protected payments (#4)
SourceSenseiTheRealOne Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
74 changes: 71 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
name: Frontend CI
name: Foundation CI

on:
push:
branches: [main, staging, development]
paths:
- "frontend/**"
- "backend/**"
- "openapi/**"
- "compose.yaml"
- "compose.production.yaml"
- "scripts/**"
- "supabase/migrations/**"
- ".github/workflows/ci.yml"
- ".github/workflows/release-images.yml"
pull_request:
branches: [main, staging, development]
paths:
- "frontend/**"
- "backend/**"
- "openapi/**"
- "compose.yaml"
- "compose.production.yaml"
- "scripts/**"
- "supabase/migrations/**"
- ".github/workflows/ci.yml"
- ".github/workflows/release-images.yml"

permissions:
contents: read

concurrency:
group: frontend-ci-${{ github.workflow }}-${{ github.ref }}
group: foundation-ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
frontend:
name: Frontend verification
runs-on: ubuntu-latest
timeout-minutes: 15
Expand All @@ -43,3 +57,57 @@ jobs:

- name: Verify frontend
run: npm run verify

backend:
name: Backend verification
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: backend
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Go
uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16
with:
go-version-file: backend/go.mod
cache: false

- name: Test backend
run: go test ./...

topology:
name: Container topology verification
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Validate Compose topology
run: docker compose -f compose.yaml config --quiet

- name: Validate production topology
env:
JUNTLY_API_IMAGE: example.invalid/juntly-api@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
JUNTLY_FRONTEND_IMAGE: example.invalid/juntly-frontend@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
DATABASE_URL: postgres://user:password@database.invalid/juntly
CLERK_SECRET_KEY: test-only
CLERK_AUTHORIZED_PARTIES: https://staging.example.invalid
JUNTLY_CONTACT_ENCRYPTION_KEY: MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: pk_test_only
STRIPE_SECRET_KEY: sk_test_ci_only
STRIPE_WEBHOOK_SECRET: whsec_ci_only
JUNTLY_PUBLIC_ORIGIN: https://staging.example.invalid
JUNTLY_PLATFORM_FEE_BPS: "1000"
run: docker compose -f compose.production.yaml config --quiet

- name: Validate operational scripts
run: sh -n scripts/backup-postgres.sh scripts/restore-postgres.sh scripts/smoke.sh

- name: Validate immutable-image release policy
run: |
python scripts/verify-release-workflow.py
python -m unittest scripts/verify-release-workflow_test.py
88 changes: 88 additions & 0 deletions .github/workflows/release-images.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Publish immutable images

on:
workflow_dispatch:
inputs:
ref:
description: Git ref to build
required: true
default: main
type: string

permissions:
contents: read
packages: write

concurrency:
group: publish-images-${{ inputs.ref }}
cancel-in-progress: false

jobs:
build:
name: Build ${{ matrix.name }} image
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- name: api
image: juntly-api
context: ./backend
dockerfile: ./backend/Dockerfile
- name: frontend
image: juntly-frontend
context: ./frontend
dockerfile: ./frontend/Dockerfile
steps:
- name: Check out requested revision
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref }}
persist-credentials: false

- name: Resolve immutable identity
id: source
shell: bash
run: |
set -euo pipefail
source_sha=$(git rev-parse HEAD)
owner=$(printf '%s' "$GITHUB_REPOSITORY_OWNER" | tr '[:upper:]' '[:lower:]')
printf 'sha=%s\n' "$source_sha" >> "$GITHUB_OUTPUT"
printf 'owner=%s\n' "$owner" >> "$GITHUB_OUTPUT"

- name: Set up Buildx
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0

- name: Authenticate to GHCR
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}

- name: Build and publish immutable image
id: image
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
platforms: linux/amd64
push: true
provenance: false
sbom: false
tags: ghcr.io/${{ steps.source.outputs.owner }}/${{ matrix.image }}:${{ steps.source.outputs.sha }}
labels: org.opencontainers.image.revision=${{ steps.source.outputs.sha }}
cache-from: type=gha,scope=${{ matrix.name }}
cache-to: type=gha,mode=max,scope=${{ matrix.name }}

- name: Record published digest
shell: bash
env:
IMAGE_DIGEST: ${{ steps.image.outputs.digest }}
IMAGE_NAME: ghcr.io/${{ steps.source.outputs.owner }}/${{ matrix.image }}
SOURCE_SHA: ${{ steps.source.outputs.sha }}
run: |
set -euo pipefail
test -n "$IMAGE_DIGEST"
printf '### %s\n\n- Source: `%s`\n- Digest: `%s@%s`\n' "$IMAGE_NAME" "$SOURCE_SHA" "$IMAGE_NAME" "$IMAGE_DIGEST" >> "$GITHUB_STEP_SUMMARY"
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,23 @@ dist/
build/
coverage/
*.tsbuildinfo
__pycache__/
*.py[cod]

# Go and test output
bin/
*.test
*.out
*.exe

# Local data and generated service state
supabase/.branches/
supabase/.temp/
supabase/.env
.volumes/
backups/
*.dump
*.dump.sha256

# Agent/tool local state
.codegraph/
Expand Down
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,36 @@ Juntly is a Portuguese local-services marketplace designed to help people discov

## Current status

This repository is at the foundation stage. The initial delivery contains:
This repository contains the production-oriented marketplace MVP:

- Durable product, architecture, security, UI, workflow, and decision context.
- A localized responsive Next.js frontend shell under `frontend/` after the scaffold commit.
- pt-PT default, English support, and Spanish-ready routing/messages.
- Frontend test, format, lint, type, build, audit, CI, and runtime-verification foundations.
- A source-level Clerk frontend identity foundation: localized sign-in/sign-up routes, session-aware navigation, and a server-enforced account route.
- A versioned OpenAPI health contract, generated TypeScript client, same-origin BFF, and narrow Go health API under `backend/`.
- Durable users, provider profiles, listings, discovery, private contact reveal, messaging, quotations, bookings, verified reviews, promotions, subscriptions, moderation, and bounded administration analytics.
- Supabase/PostgreSQL migrations and a local frontend/API Compose topology that connects to an explicitly supplied database.
- A digest-oriented production Compose topology, dependency readiness probe, hardened HTTP/runtime defaults, backup/restore scripts, smoke checks, and operational runbooks.
- Frontend test, format, lint, type, build, dependency-audit, CI, and runtime-verification foundations.

It does **not** yet implement accounts, provider profiles, listings, search, chat, quotations, bookings, reviews, payments, Go/OpenAPI, Clerk, Supabase, Redis, object storage, Docker, or production deployment.
Paid subscriptions and promotions intentionally remain pending until an external payment provider confirms them; free configured entries activate immediately. Production deployment still requires operator-supplied infrastructure, secrets, DNS/TLS, a managed PostgreSQL target, and a real Clerk test account for authenticated acceptance journeys.

## Repository layout

```text
juntly/
├── frontend/ Next.js frontend (created by the scaffold commit)
├── backend/ Go API health-tracer foundation
├── openapi/ versioned API contracts
├── context/ sanitized durable project reference
├── compose.yaml local frontend/API development topology
├── compose.production.yaml hardened immutable-image topology
├── supabase/ ordered PostgreSQL migrations
├── scripts/ backup, restore, and smoke operations
└── AGENTS.md project operating rules
```

`backend/` and `supabase/` will be created only when the approved API-foundation slice implements them.
Deployment and recovery procedures live in [`docs/operations/`](docs/operations/).

## Context

Expand Down
8 changes: 8 additions & 0 deletions backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.git
.env
.env.*
!.env.example
*.key
*.pem
*.log
bin/
20 changes: 20 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Server-only Go API configuration. Keep real values in ignored backend/.env.local or the service environment.
DATABASE_URL=
CLERK_SECRET_KEY=
# Optional: server-only AES-256-GCM key for contact reveal. Base64-encoded 32 bytes.
JUNTLY_CONTACT_ENCRYPTION_KEY=
# Optional: enables networkless Clerk JWT verification when configured.
CLERK_JWT_KEY=
# Comma-separated exact allowed Clerk authorized-party origins.
CLERK_AUTHORIZED_PARTIES=http://localhost:4200
# Optional Go duration, capped at 30s. Leave empty unless a local clock cannot synchronize.
CLERK_CLOCK_SKEW=
# Stripe payments remain disabled when all fields below are empty. If any field is set,
# all required fields must be present or API startup fails closed.
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# Optional test-only API override. Production uses https://api.stripe.com.
STRIPE_API_BASE=
JUNTLY_PUBLIC_ORIGIN=https://somosvila.com
# Reviewed commission in basis points (1000 = 10%). Do not derive this in the browser.
JUNTLY_PLATFORM_FEE_BPS=
21 changes: 21 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM golang:1.26.6-alpine AS build

WORKDIR /src

COPY go.mod ./
RUN go mod download

COPY . ./
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w -X main.version=0.1.0" -o /out/juntly-api ./cmd/api

FROM alpine:3.22

RUN apk add --no-cache ca-certificates && addgroup -S juntly && adduser -S -G juntly juntly

COPY --from=build /out/juntly-api /usr/local/bin/juntly-api

USER juntly
EXPOSE 8080
HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=5 CMD wget -q -O /dev/null http://127.0.0.1:8080/api/v1/ready || exit 1

ENTRYPOINT ["/usr/local/bin/juntly-api"]
83 changes: 83 additions & 0 deletions backend/cmd/api/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package main

import (
"errors"
"strconv"
"strings"
"time"

"github.com/SourceSenseiTheRealOne/juntly/backend/internal/authn"
"github.com/SourceSenseiTheRealOne/juntly/backend/internal/contactreveal"
"github.com/SourceSenseiTheRealOne/juntly/backend/internal/payments"
)

var ErrInvalidRuntimeConfig = errors.New("invalid API runtime configuration")

type runtimeConfig struct {
databaseURL string
verifier authn.Verifier
contactCipher contactreveal.Cipher
paymentGateway payments.Gateway
platformFeeBPS int
}

func loadRuntimeConfig(lookup func(string) string) (runtimeConfig, error) {
databaseURL := strings.TrimSpace(lookup("DATABASE_URL"))
if databaseURL == "" {
return runtimeConfig{}, ErrInvalidRuntimeConfig
}
clockSkew, err := parseOptionalDuration(lookup("CLERK_CLOCK_SKEW"))
if err != nil {
return runtimeConfig{}, ErrInvalidRuntimeConfig
}

verifier, err := authn.NewClerkVerifier(authn.ClerkVerifierConfig{
SecretKey: lookup("CLERK_SECRET_KEY"),
JWTKey: lookup("CLERK_JWT_KEY"),
AuthorizedParties: strings.Split(lookup("CLERK_AUTHORIZED_PARTIES"), ","),
ClockSkew: clockSkew,
})
if err != nil {
return runtimeConfig{}, ErrInvalidRuntimeConfig
}
var contactCipher contactreveal.Cipher
if encodedKey := strings.TrimSpace(lookup("JUNTLY_CONTACT_ENCRYPTION_KEY")); encodedKey != "" {
contactCipher, err = contactreveal.NewCipher(encodedKey)
if err != nil {
return runtimeConfig{}, ErrInvalidRuntimeConfig
}
}
var paymentGateway payments.Gateway
platformFeeBPS := 0
stripeSecret := strings.TrimSpace(lookup("STRIPE_SECRET_KEY"))
stripeWebhook := strings.TrimSpace(lookup("STRIPE_WEBHOOK_SECRET"))
publicOrigin := strings.TrimSpace(lookup("JUNTLY_PUBLIC_ORIGIN"))
feeValue := strings.TrimSpace(lookup("JUNTLY_PLATFORM_FEE_BPS"))
if stripeSecret != "" || stripeWebhook != "" || publicOrigin != "" || feeValue != "" {
if stripeSecret == "" || stripeWebhook == "" || publicOrigin == "" || feeValue == "" {
return runtimeConfig{}, ErrInvalidRuntimeConfig
}
platformFeeBPS, err = strconv.Atoi(feeValue)
if err != nil || platformFeeBPS < 0 || platformFeeBPS >= 10_000 {
return runtimeConfig{}, ErrInvalidRuntimeConfig
}
apiBase := strings.TrimSpace(lookup("STRIPE_API_BASE"))
if apiBase == "" {
apiBase = "https://api.stripe.com"
}
paymentGateway, err = payments.NewStripeGateway(payments.StripeConfig{SecretKey: stripeSecret, WebhookSecret: stripeWebhook, APIBase: apiBase, PublicOrigin: publicOrigin, Now: time.Now})
if err != nil {
return runtimeConfig{}, ErrInvalidRuntimeConfig
}
}

return runtimeConfig{databaseURL: databaseURL, verifier: verifier, contactCipher: contactCipher, paymentGateway: paymentGateway, platformFeeBPS: platformFeeBPS}, nil
}

func parseOptionalDuration(value string) (time.Duration, error) {
value = strings.TrimSpace(value)
if value == "" {
return 0, nil
}
return time.ParseDuration(value)
}
Loading