Platform Technical
Architecture &
Engineering Overview

This document covers the full technical stack of the Oui Are Makers UGC tutorial-hosting platform — France's largest DIY knowledge-sharing network — as it operated circa 2020–2021. Written from the CTO perspective for engineering teams, investors, and technical partners.

21 680
Source lines of Ruby
The application is primarily written in Ruby — a single, expressive, well-tested codebase.
99.65% Ruby
10K+
Tutorials hosted
14K
Community members
7
Content verticals
2×
Platform products

Rails monolith on Linux · PostgreSQL · S3

The platform is a Ruby on Rails monolith — a deliberate choice that maximises developer velocity for a small team while keeping the deployment surface minimal. Two interconnected products share a common user layer: tutos.ouiaremakers.com (tutorial CMS + community) and Les Ateliers (local maker marketplace). The stack runs on a hardened Linux server with PostgreSQL as the primary datastore and AWS S3 for all media assets.

System Architecture · tutos.ouiaremakers.com · ~2021 Ruby on Rails 6 · PostgreSQL · S3
Clients
Web Browser
Desktop / Mobile · Turbolinks
Social Embeds
Open Graph / oEmbed
SEO Crawlers
Googlebot · Bingbot
Edge
Nginx (reverse proxy)
TLS termination · rate-limiting · gzip
CloudFront CDN
S3 assets · thumbnails · static files
App
Rails app (Puma)
Tutorial CMS · Community · SaaS router
Les Ateliers (same app)
Marketplace · Booking · Geo-search
Sidekiq
Background jobs · Email · Media jobs
Services
Stripe
Payments · Webhooks · Checkout
Plausible (self-hosted)
Privacy-first analytics · own server
Open Graph
Social sharing · og: meta
Brevo / Sibforms
Newsletters · Transactional email
Data
PostgreSQL
Primary datastore · one DB per SaaS tenant
AWS S3
UGC media · thumbnails · backups
Redis
Sidekiq queue · ActionCable · cache
Architecture note
The Rails monolith gives a single deployable unit, shared business logic, and Rails conventions the whole team speaks. The tradeoff — coupling the tutorial CMS and the Ateliers marketplace — was accepted as worthwhile at this scale. ActiveRecord models, concerns, and service objects cleanly separate responsibilities without microservices overhead.

Tutorial Platform — UGC at scale

The core product is a UGC platform for step-by-step tutorials across seven verticals. Makers author multi-step tutorials with rich media, publish them publicly, and receive community feedback. Client-side media optimisation (resize, compress, WebP conversion) runs in the browser before upload, keeping S3 storage lean and reducing CDN egress costs significantly.

📸 Client-side media pipeline
Before any file reaches the server, a JavaScript pipeline runs in-browser: images are resized to maximum display resolution, recompressed (target ≤ 200 KB per step photo), and converted to WebP. Only then is a pre-signed S3 URL requested from Rails, and the optimised file is uploaded directly from the browser — the server never handles raw binary. This eliminates server-side image-processing bottlenecks and cuts S3 storage by ~70% versus raw uploads.
🪚
Bricolage
DIY woodworking, home repairs, furniture builds
💻
Technologie
Arduino, Raspberry Pi, 3D printing, electronics
🎨
Art & Craft
Handmade, illustration, macramé, upcycling
🍳
Cuisine
Recipes, food projects, preserving techniques
👗
Mode & Beauté
Sewing, upcycled fashion, DIY cosmetics
🪴
Décoration
Home décor, gardening, seasonal projects
🧸
Enfants
Family-safe crafts, educational projects
01
Registration & Profile
Devise auth with email confirmation. Maker profile: skill tags, bio, portfolio. Passwords BCrypt-hashed; strong-password validation on Devise model.
Devise
02
Tutorial Creation
Step-by-step editor backed by nested has_many :steps in ActiveRecord. Fields: title, cover photo, materials list, difficulty enum, estimated duration, ordered steps with media.
ActiveRecord
03
Client-side Optimisation & Direct S3 Upload
Browser JS resizes, compresses, and WebP-converts images before upload. Pre-signed S3 URL issued by Rails; binary goes directly to S3 — server never touches raw media. ActiveStorage tracks attachments in PostgreSQL.
S3 · ActiveStorage
04
Taxonomy & Tagging
Category, free-form tags via acts-as-taggable-on, difficulty enum, duration. Drives discovery, curated collections, and SEO landing pages.
acts-as-taggable
05
Publication & SEO
Auto-generated Open Graph tags via meta-tags gem, JSON-LD structured data for rich snippets, canonical URLs, sitemap via sitemap_generator pinged on publish.
OG · JSON-LD
06
Community Signals
Likes, comments (Commontator), saves — feeding ranking algorithm for monthly editorial curation. Counters cached in Redis to avoid expensive COUNT queries at load time.
Redis cache

Maker reputation system

Community is the product's moat. Beyond Devise auth, we built a maker reputation layer — profile badges, "Maker à Suivre" recognition, challenge scores, and public portfolios — driving content production without editorial cost.

EntityDescriptionRails implementationStatus
maker_profilePublic portfolio: tutorials, badge wall, bio, skill tagsUser model + JSON columns + concernsprod
badge_system"Maker à Suivre", challenge winner, top contributorPolymorphic BadgeAward modelprod
challenge_entryThemed competitions with community votingChallenge + Entry + Vote modelsprod
comments / likesPer-tutorial reactions and qualitative feedbackCommontator gem + Likeable concernprod
private_corpo_spaceWhite-label tutorial sharing for enterprise clientsSaaS tenant scope (see below)beta
newsletter_segmentsMaker newsletter + corporate newsletter via BrevoBrevo API + ActiveJob on subscribeprod
⚠ Engineering note
User identity is shared between the public tutorial platform and the Ateliers marketplace via the same Devise User model. Role management (marketplace provider vs. content creator) is handled through a bitmask role field and Pundit policies. For the SaaS multi-tenant version, a scoped User model per subdomain tenant ensures complete data isolation.

Maker Marketplace — booking & geo-search

The second product is a geolocation-based peer-to-peer marketplace connecting makers offering workshops with learners nearby. Geo-search is powered by the Geocoder gem with PostGIS extensions on PostgreSQL for radius queries.

📍
Geo Listings
PostGIS + Geocoder gem for radius-based atelier discovery across France
🔎
Faceted Search
Filter by city, skill, availability, price — Ransack gem on PostgreSQL scopes
💳
Stripe Payments
Stripe Checkout sessions created server-side; webhook updates booking state
🛠️
Tool Lending
Makers list tools for local loan — same CRUD surface as workshop listings
🏅
Provider Profiles
Cross-linked with tutorial portfolio — single profile, dual role
📬
Messaging
ActionMailer notifications; in-app messaging queued via Sidekiq + Redis

Fully private, white-label instances per client

Beyond the public platform, a SaaS tier delivers completely private, isolated instances of the application for enterprise clients. Each tenant gets their own subdomain, their own dedicated PostgreSQL database, and their own S3 bucket prefix — no data ever touches another tenant's namespace.

🌐 Subdomain-per-tenant routing
Nginx + Rails

Each client is provisioned on a dedicated subdomain (client.ouiaremakers.com). Nginx handles TLS via a Let's Encrypt wildcard cert (DNS-01 challenge). The Rails router resolves the subdomain to a tenant record, then switches the ActiveRecord connection to that tenant's dedicated PostgreSQL database using the Apartment gem.

Wildcard TLS Apartment gem DB per tenant
🗄 Database-per-tenant isolation
PostgreSQL

Each SaaS tenant has a separate PostgreSQL database (not just a schema). Hard data boundaries, independent backup/restore, independent migration cadence, and zero risk of cross-tenant query leakage. A shared public schema holds only tenant registry metadata.

Hard isolation Independent backup Per-tenant migrations
✉ Email-whitelist account creation
Access control

Registration on a private tenant is guarded by an email domain/address whitelist stored in tenant configuration. A Devise before_create hook validates the registering address; non-whitelisted addresses receive a polite denial without revealing the tenant exists. Admins manage the whitelist via the tenant admin UI.

Domain whitelist Devise hook Admin-managed
☁ S3 bucket-prefix per tenant
AWS S3

All media uploads are namespaced under a tenant-specific S3 prefix (s3://bucket/tenants/{slug}/…). IAM policies enforce that each application role reads/writes only its own prefix. CloudFront distributions are configured per-tenant — assets are never accessible cross-tenant.

IAM prefix policy CloudFront per tenant ActiveStorage scoped
SaaS provisioning flow
A new tenant is provisioned by a Rails admin rake task: creates the tenant record, creates the PostgreSQL database, runs migrations, seeds default categories and configuration, provisions the S3 prefix, and outputs the Nginx snippet for subdomain routing. Full provisioning takes under 90 seconds. Deprovisioning exports a dump to S3 before dropping the database.

Hardened Debian server · defence in depth

The platform runs on a dedicated Debian Linux server (bare-metal / VPS). The security posture follows a defence-in-depth model: minimal attack surface at every layer, with automation keeping the surface controlled over time.

🐧
Debian (LTS) base
Minimal install, no GUI. unattended-upgrades for security patches. Kernel hardened with sysctl tuning (IP forward disabled, SYN cookies, core dump disabled).
🔒
SSH hardening
Key-only auth, root login disabled, non-standard port, AllowUsers whitelist, fail2ban blocking brute-force within 5 attempts.
🧱
UFW + iptables firewall
Default-deny inbound; only ports 80, 443, and SSH open. Outbound restricted to required S3 endpoints and Stripe IPs.
🌐
Nginx + TLS/HTTPS
TLS 1.2/1.3 only, HSTS with preload, OCSP stapling, A+ SSL Labs rating. Let's Encrypt certs auto-renewed via certbot systemd timer.
👤
Least-privilege Unix users
Rails app runs as dedicated deploy user with no shell. PostgreSQL accessed via scoped role — no superuser rights. Sidekiq under same user.
📦
Capistrano deploy
Atomic releases via symlink swap, automatic rollback on failed migration. Shared .env credentials never in Git — injected at deploy time.
🔐
Rails encrypted credentials
All secrets in Rails encrypted credentials file. Master key injected via environment variable — never stored on disk unencrypted. Separate credential files per environment.
💾
PostgreSQL backups
Nightly pg_dump compressed and uploaded to a private S3 bucket. 30-day retention. Restore tested monthly in staging.
🚦
Rack::Attack rate-limiting
Request throttling by IP on login, signup, API, and file-upload endpoints. Redis-backed counters — blocked requests return 429 without touching the Rails stack.
🛡️
Rails security defaults
Brakeman SAST in CI pipeline. CSRF protection on all state-changing requests. Content-Security-Policy via SecureHeaders gem. Dependabot alerts on gems.
📡
Logwatch + monitoring
Daily Logwatch digest, health-check endpoint polled by UptimeRobot. PagerDuty alert on Puma process death. Syslog shipped to local ELK instance.
🔑
IAM & S3 security
S3 bucket policy: public access blocked at account level. All object access via signed URLs or CloudFront with OAI. Per-tenant IAM roles with scoped prefix policies.

GitLab CI · Cucumber · Selenium · automated push to prod

Every push triggers a GitLab CI pipeline that must pass all stages before code reaches production. The test suite is built on Cucumber + Selenium, providing full BDD end-to-end coverage alongside RSpec unit and integration tests. A green pipeline on main automatically deploys to production via Capistrano — zero manual intervention.

01
lint
RuboCop (style + linting), ERB Lint (template hygiene), Brakeman (SAST security scan). Fast fail — catches issues before any test runner starts.
~2 min
02
unit & integration tests
RSpec model, controller, and service-object specs. Parallel execution across 4 CI runners. DatabaseCleaner between examples. Factory Bot fixtures — no fixtures.yml anywhere.
~6 min
03
cucumber / selenium e2e
Full BDD scenario coverage: tutorial authoring, challenge submission, marketplace booking, Stripe checkout (test mode), SaaS tenant provisioning, email-whitelist enforcement, Plausible event firing. Headless Chrome via Selenium WebDriver. Scenarios in Gherkin — readable by non-engineers and used as living documentation.
~14 min
04
assets & build
Webpacker / Asset Pipeline precompile, CSS minification, fingerprinting. Build artefact archived to GitLab registry for consumption by the deploy stage.
~3 min
05
deploy (main branch only)
Capistrano deploy triggered by GitLab CI on main after all prior stages pass. Atomic symlink swap, pending migrations run, Puma gracefully restarted, Sidekiq restarted. Automatic rollback if migrations fail. Total deploy window: ~90 seconds, zero downtime.
auto · ~90s
Test coverage philosophy
Cucumber scenarios describe the system from the user's perspective — they are the living specification. Every major user journey (tutorial creation, challenge submission, atelier booking, SaaS tenant login, whitelist rejection) has at least one full Selenium scenario. Regressions are caught before they ever reach production, not after.

Self-hosted Plausible · privacy-first · GDPR-clean

Analytics is powered by a self-hosted and self-managed Plausible instance running on the same Linux infrastructure. Plausible collects no personal data, sets no cookies, and requires no consent banner — making the platform natively GDPR-compliant without popup friction. All data stays on our own server; nothing is sent to any third party.

🏠
Self-hosted instance
Plausible CE deployed via Docker Compose on the production server. Data stored in ClickHouse. Zero dependency on external analytics services.
🍪
No cookies · no consent banner
Plausible uses no cookies and collects no personal data. No analytics consent popup required — lower friction, cleaner UX, fully RGPD-compliant per CNIL guidance.
📊
Key metrics tracked
Pageviews, unique visitors, tutorial completion proxies (scroll depth), category engagement, referral sources, campaign UTMs, Ateliers booking funnel.
🎯
Custom events
Tutorial step view, challenge entry submitted, atelier booked, newsletter signup — tracked via Plausible's lightweight JS event API without any PII.
🔒
Data sovereignty
All analytics data on our server in France. No data processed by Google, Meta, or any US-based service. Fully aligned with CNIL cookieless analytics guidance.
🔄
SaaS tenant dashboards
Each SaaS tenant has its own Plausible site key. Traffic data isolated per tenant — clients see only their own audience data via embedded Plausible iframe in admin UI.

Stripe Checkout · zero card data on server

Stripe Checkout (hosted page) keeps PCI scope minimal — no card data touches the Rails app. Provider payouts in v1 are manual; Stripe Connect is queued for v2 full marketplace automation.

01
Atelier selected & seat validated
Rails controller checks remaining capacity via a PostgreSQL row-level lock before opening a checkout session. Prevents overselling under concurrent bookings.
PG lock
02
Stripe Checkout Session created
Server-side call to Stripe API with line_items, metadata (atelier_id, tenant_id, provider_id), success/cancel URLs. Session ID cached in Redis with 30-minute TTL.
Stripe API
03
Payment on Stripe-hosted page
Learner pays on Stripe's domain. Zero card data touches OAM servers. PCI scope reduced to SAQ A.
PCI scope ↓
04
Webhook → booking confirmation
checkout.session.completed received, verified by Stripe signature. Sidekiq job updates Booking record, fires ActionMailer to learner and provider, decrements seat count.
Sidekiq + sig
05
Provider payout (manual v1)
Monthly manual transfer via Stripe Dashboard. Stripe Connect is the natural v2 upgrade for automated marketplace splits without custom payout logic.
v2 roadmap
Menu