Pixilence
An Arabic-first learning platform, designed and built solo in two months on a passion-project budget. Right to left throughout, two apps on one database, and a completion path that ends in a verifiable certificate.
- Product Design
- Design Systems
- Full-Stack Build
- Arabic / RTL

Where it came from
A YouTube channel that outgrew YouTube
I started publishing in January 2023: the fundamentals of UX, colour theory, interface design, a three-part Figma course running beginner to advanced, a Webflow course. The channel is at roughly 20,000 subscribers.
The problem is that YouTube is not a place to learn something in order. It is a place to be recommended something else. There is no sequence, no record of what you finished, and a sidebar whose entire job is to interrupt you. I was asking people to follow a curriculum on a platform built to break curricula.
Pixilence is that curriculum somewhere it can hold its shape: Arabic first, no recommendations, progress that persists, and a route through the material that ends in something you can show an employer.

Arabic first
Right to left is the easy five percent
Setting dir="rtl" takes a minute. What takes the time is everything Latin typography lets you take for granted.
Arabic letters connect. Letter-spacing pulls them apart and breaks the word, so the standard trick of tracking out a small uppercase eyebrow is simply unavailable. Emphasis at small sizes has to come from weight instead, which is why picking a family with enough real weights mattered more here than in any Latin project I have built.
Then the mixed strings. Latin numerals sitting inside an Arabic sentence reorder the line, because the two scripts run in opposite directions and the numerals are their own bidirectional case. Course durations, lesson counts, percentages and dates all needed handling rather than hoping. Line heights differ too, because Arabic has ascenders and descenders in different places than Latin does.
Faces
Thmanyah Sans, five weights, Thmanyah Serif Display
Delivery
Self-hosted OTF, font-display: swap, Licensed for commercial use
Emphasis
Weight, never letter-spacing
Direction
dir=rtl on both apps, Bidirectional numerals, Mirrored iconography

Weight carries emphasis where tracking would break the word.

Mint on near-black. One accent, dark only, no light theme.
The shape of it
Two apps, one repository, one database
A Turborepo holds two Next apps that deploy to two domains. pixilence.com is the public marketing site. members.pixilence.com is everything behind the login. They share a UI package, a Supabase client package and a config package, so a component is written once and a token changes in one place.
The part I am happiest with is that the admin writes once and both sites read. Publishing a course populates the members area and, at the same time, the public course page on the marketing site: the outline, the modules, the lesson list, the attached expert. Visitors can see exactly what they are signing up for, and I never maintain the same content twice. Blog posts work the same way.
Monorepo
Turborepo, pnpm workspaces, apps/web, apps/members, packages/ui, supabase, config
Framework
Next.js App Router, React, TypeScript, Tailwind CSS
Data and auth
Supabase Postgres, Supabase Auth, 17 tables, 11 migrations
Services
Vercel, Resend, Supabase Edge Functions on Deno, YouTube embeds
Interface
framer-motion, lucide-react, next-themes
Documents
@react-pdf/renderer, qrcode, next-mdx-remote

Public catalogue, rendered from the same rows the members area reads.

The full outline is public. You can see the course before you join.

Free registration. Supabase Auth.

Confirmation mail through Resend, so signups cannot be abused.
The members area
A route, not a library
The dashboard opens on the courses you are enrolled in, the most recent three, alongside upcoming live sessions and the newest videos. It answers one question: what were you doing.
Around the courses sit three things that are deliberately not courses. A video library, for the pieces worth keeping that belong to no curriculum. Live sessions, recorded and posted afterwards. Resources, which are just links and files, because not everything worth giving someone is a video.

Dashboard. Enrolled courses, upcoming live sessions, newest videos.

Course list, enrolled and available.

Modules and lessons, with the attached expert profile.

Lesson player.

Video library, for what belongs to no course.

Live sessions, recorded and posted after.

Resources: links and files, not videos.

Account settings, email and password changes.
Completion
The part that makes it a course and not a playlist
Every lesson a member finishes is recorded against them. When every lesson in a course is complete, the course issues a certificate.
Quizzes carry the weight in between. They are multiple choice and mark themselves: I write the questions and the correct answers in the admin, single or multiple, and each quiz stores its own pass mark, which is 75% by default. Some sit inside modules as lessons in their own right, and each course can carry a final at the end.
The certificate renders to PDF in the browser and carries a QR code pointing at a public verification page on the marketing site. Anyone handed the certificate can check it against the database without an account. That is the difference between a completion badge and something worth putting in front of an employer, and it is the one feature here that Ecom Society has nothing like.

Self-marking multiple choice. Pass mark is stored per quiz.

Rendered to PDF in the browser, QR code embedded.

Public verification. No account needed to check a certificate.

Blog, also written once in the admin.
The admin
Built so I never have to open the code to publish
Courses, modules and lessons, quizzes and their answer keys, the video library, live sessions, resources, blog posts and expert profiles are all managed from an admin only I can reach. Experts are their own records, with a photo, a name, a title and a short bio, attached to a course from a dropdown and rendered on both sites.
None of this was necessary for one person shipping alone. I built it because editing content by editing code is a habit that stops working the moment anyone else joins, and the plan is to bring someone in to help run the platform. Ecom Society taught me that lesson the expensive way, by making every content change a developer task.

Admin overview.

Course, module and lesson editor.

Quiz builder, questions and answer keys.

Expert profiles, attached to courses from a dropdown.
Constraint
No Replit, and that was the point
Ecom Society lives on Replit, where hosting, database and deployment are one thing you never have to think about. Pixilence is a passion project with no revenue, so paying a premium to host something I am not actively changing made no sense.
So every piece is stitched by hand: Supabase for Postgres and auth, Resend for transactional mail, Deno edge functions for the contact form and auth emails, Vercel for two deployments off one repository. The integration work that Replit had been quietly doing for me became the work, and doing it taught me more in two months than the previous year had: APIs, webhooks, SQL, migrations, what a policy actually is.
Video is where the budget shows. Dedicated hosting prices out a pre-revenue project, so lessons are unlisted YouTube videos embedded in the player. It is not the right long-term answer, the content is only as private as an unlisted URL, and it moves to a real host the moment the platform earns anything. Naming that is more useful than pretending it was a design decision.
Security
The thing I was most afraid of
Row level security intimidated me. I did not understand what it was protecting against, and authentication is not somewhere to guess. So I stopped and learned Postgres policies properly.
The result is 58 policies across the schema, with row level security enabled on every table that holds anything personal. A member can read their own enrolments, their own lesson progress, their own quiz attempts and their own certificates, and structurally cannot read anyone else’s. Admins get their own explicit read policies rather than a bypass. This is enforced by the database, not by a check in the interface that a bug can route around.
There are no users on this platform yet. Doing it properly before anyone arrived is the entire point: it is much harder to retrofit isolation onto a table people are already writing to.
What went wrong
I modelled the data before I understood it
The first schema was wrong. I designed tables around the screens I had drawn instead of around what the product actually needed to know, and spent weeks discovering missing columns one feature at a time, then writing another migration. Eleven migrations exist largely because the first one was optimistic.
Authentication broke too, and had to be pulled apart and rebuilt. Clerk was on the list before I chose Supabase Auth, and the research time was not wasted, but the first integration was not right and the second one was.
Both mistakes have the same shape: I moved fast on the part that felt like plumbing, and slowly on the part that felt like design. On this project the plumbing was the design.
~20,000
YouTube subscribers the platform was built for
2 months
Solo, alongside a full-time role
58
Row level security policies protecting member data

Where it is now
Finished, live, and deliberately quiet
Both sites are deployed and working. Registration is open and free. Nothing is being marketed, because pointing an audience at an empty library is worse than not pointing them anywhere. Two courses are planned, artwork for the first is done and loaded, and the recording is what stands between this and launch.
The paid tier is designed but not built. Beginner courses stay free permanently; intermediate and advanced go behind Stripe once there is enough there to charge for. Building that now would be building for a transaction nobody can make yet.
Ecom Society proved I could take over a product someone else built. This one proved I could start from nothing, in a language and a direction that breaks most Western interface assumptions, on a budget that ruled out the easy path, and finish it.
Next case study