Go back to articles

Shipping the Projects Feature: From Schema to Showcase

How I designed and built a full projects portfolio feature — from database schema and many-to-many skill associations to a media library picker and public showcase pages — in a single focused session.

Shipping the Projects Feature: From Schema to Showcase

March 19, 2026 — One of the items on my roadmap since the very beginning was a proper projects showcase. Today it shipped — from empty database table to fully functional admin UI and public-facing portfolio grid. Here’s how it came together.

The Requirements

Before writing any code, I mapped out what a “project” actually needs to represent in the context of a professional portfolio:

  • Title and slug for public URLs
  • Description (Markdown) and excerpt for cards
  • Status: active, inactive, completed, archived — because not every project is eternal
  • Start/end dates — with “active” projects having no end date
  • External URL — many projects live elsewhere (GitHub, company sites)
  • Logo/hero image — selected from the media library, not just a URL field
  • Tags — freeform labels like “open-source”, “web”, “data”
  • Skills — linked to the global skills table via many-to-many
  • Work Experience link — optionally associate a project with a company/institution
  • Image gallery — for project screenshots and visuals

The Schema Design

Three tables were needed:

projects

The main table with all the metadata — title, slug, description, excerpt, external_url, status (Ecto Enum), dates, logo_url, tags (Postgres array), published flag, display_order, and an optional work_experience_id foreign key.

project_skills

A join table for the many-to-many relationship with skills. Just two foreign keys with a unique index — simple but powerful.

project_images

A gallery table that supports both media library references (media_id) and direct URLs.

The Ecto Schema: Associations Done Right

many_to_many :skills, Skill,
  join_through: "project_skills",
  on_replace: :delete

This is the key trick — when you update a project’s skills, Ecto automatically handles inserting and deleting join table rows.

The Admin UI: Media Library Picker

The most interesting UI challenge was the logo image selector — a preview thumbnail, a “Choose Image” button opening a modal with URL input and a scrollable grid of media library images, plus a “Remove” button.

The skills selector uses checkbox cards in a responsive grid — each skill gets a checkbox, and checked skills are tracked in a @selected_skill_ids assign.

The Public Pages

Projects Index (/projects)

A responsive grid (1→2→3 columns) where each project card shows logo, title, date range, excerpt, skill pills (purple), and tag pills (teal).

Project Detail (/projects/:slug)

A single-column article-style layout with title, metadata, skills, tags, Markdown description, and image gallery.

What I Learned

  1. Many-to-many in Ecto is elegant with put_assoc and on_replace: :delete
  2. HTML forms send "" for empty selects — scrub before Ecto cast
  3. URL validation must accept local /uploads/ paths when serving media internally
  4. Reusable admin patterns accelerate every new feature

From empty table to public portfolio in one session — that’s the Phoenix developer experience.