Easy-to-manage touch screens
Free, open-source touch screens. Guided USB setup, a drag-and-drop editor and local control. Made for Home Assistant.
Apunta lo que sabes de cada persona y sus fechas. PickPal te avisa cuando se acercan y te propone ideas de regalo a partir de lo que apuntaste.
Translation pending
LifestyleTypeScript · JavaScriptGitHub @jm-fusterOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Birthday & anniversary reminders with AI-powered gift ideas. Open source, built with Next.js, Convex, and Gemini.
Taken from the repository README
Loading the full README.
Read the full README ↗Whose birthday is next month?
Keep what you know about each person, decide when to be reminded, and let a model think with you when the date comes around.
A notebook for the people you care about, with an AI feature bolted on — in that order. You write down what you know about someone: their interests, their sizes, what they are allergic to, what you gave them last year and how it landed. When one of their dates approaches, PickPal reminds you and can turn everything you wrote down into gift ideas for that specific person, occasion and budget.
Built as a personal project, and used for real. The interface is in Spanish only — there is no i18n layer, the routes are Spanish words, and the model is prompted in Spanish. Everything below is the English description of a Spanish product.
An agenda that only shows the near future. /agenda lists the next four
months of events sorted by how soon they are, grouped under Hoy, Mañana or
the date itself, with urgency styling once something is inside seven days. On
wide screens, picking an event opens the gift panel beside it; on narrow ones it
navigates. A bell in the header lists whatever falls inside your notification
window — 30 days by default, and while the server accepts anything from 1 to 365,
no screen exposes that control yet.
A person is a notebook page. Name, relationship, up to 20 interests, up to 10
favourite brands, free notes, shoe and clothing sizes, allergies, dislikes, and a
generated avatar rather than an uploaded photo. The interest field autocompletes
against a local catalogue of roughly 130 interests in 17 categories
(src/lib/interests.ts) — deliberately offline, so suggestions cost no model
quota and nothing about the person leaves the device to power them. Editing is
inline autosave; /seres-queridos/[personId]/edit exists only as a redirect back
to the detail page.
Dates and history. Each date carries a label, a day and month, an optional year, a flag for annual or one-off, and an optional budget on a 0–500 € slider. Gift history records what you gave, for which occasion, in which year, how it was received, and any notes — and that history is fed back into the next generation.
Gift ideas on a short leash. Pick the occasion, pick one of four kinds of gift
(Producto físico, Experiencia, Tiempo juntos, Sorpréndeme), and press
Generar 9 ideas. The request asks Gemini 3.5 Flash for nine and accepts
between six and nine (z.array(...).min(6).max(9), src/lib/gifts.ts:88), then
drops duplicate titles. Save an idea with a thumbs-up, or discard it with a
thumbs-down — discarding offers an Undo, and once it sticks, that idea's
categories are recorded so the next batch steers away from them. A saved idea
becomes gift history through Lo regalé. Batches are cached per user, person,
occasion and gift type, so reopening a set you already generated costs no quota.
Store links. A physical-product idea deep-links into whichever of eleven
retailers you picked in settings — narrowed to the ones the model suggested for
that specific idea, falling back to all of your picks when none of them match:
Amazon, El Corte Inglés, AliExpress, Temu, Miravia, Decathlon, IKEA,
PcComponentes, MediaMarkt, Zalando and Druni (STORE_IDS, src/lib/stores.ts).
Experiences, time together and surprises get a single search link instead.
My list, the other half of the notebook. /mi-lista is where you write what
you would like to receive and share it with the people who give you presents.
They find it inside the profile they already keep of you and mark what they will
give; you never see the marks, and nobody learns who else reads. A list can start
from somewhere else — paste text from another app, a note or a message, upload a
.csv, or use a bookmarklet on a store page — and it is all parsed in the
browser: PickPal never opens the links. Items carry an approximate price, a note
and an optional photo (re-encoded in the browser to drop its location data), can
be put in a manual order, and up to five can be starred as the ones that would
make you happiest. You can also keep a list for someone with no account, such as
a child; you see what is taken, never by whom. A reader can stop receiving
someone's lists without that person ever finding out. The specs are
docs/encargo-lista.md and
docs/encargo-listas-ninos.md.
Email reminders, opt-in and off by default, because the declared legal basis
is consent. Lead times are a multi-select of 0, 2, 7 and 14 days, defaulting to
[14], and a user gets one grouped email per run rather than one per date.
Settings covers the theme, reminders, favourite stores, legal links, and
account deletion — type ELIMINAR, and it purges the Convex data before deleting
the Clerk user.
POST /api/recommendations is the part of this repo worth reading first.
POST /api/recommendations
│
├─ src/proxy.ts ─────────── default-deny matcher + Sec-Fetch-Site CSRF check
├─ auth() ───────────────── Clerk session, then a JWT for Convex
├─ env check ────────────── 503 unless GOOGLE_GENERATIVE_AI_API_KEY and
│ CONVEX_SERVER_SECRET are both present
├─ zod parse ────────────── the request body
│
├─ 4 × fetchQuery ───────── person · date and budget · gift history ·
│ (in parallel) the last batch's dislikedCategories
│
├─ reserve quota ──┐ ────── atomic, and before the model is called
│ │
├─ generateObject ─┤ ────── gemini-3.5-flash, Zod-typed output, maxRetries: 2
├─ dedupe titles ──┤
├─ enrich ─────────┤ ────── Pexels photo (4 s) · Brandfetch store (4 s / 2.5 s)
│ (best effort) │ both optional, both allowed to fail quietly
│ │
├─ upsert ─────────┤ ────── Convex re-validates every field server-side
│ │
└─ 200 JSON └─────── any throw above refunds the reserved unit
Three things in that diagram are the whole point:
api.recommendations.upsert
re-validates every field the model produced, so a compromised route handler
still cannot write nonsense into the database.src/proxy.ts — Next 16 renamed middleware.ts to proxy.ts, and
npm run build prints it as ƒ Proxy (Middleware). It runs clerkMiddleware
with a default-deny matcher: only /, /sign-in(.*), /sign-up(.*),
/privacidad and /terminos are public, and anything else hits
auth.protect(). A new route is therefore private by accident rather than
public by accident. It also 403s non-GET /api requests whose
Sec-Fetch-Site header says they came from another origin.auth() in the route handlers, with ConvexProviderWithClerk in the browser.requireUser(ctx) in every Convex function, followed by a per-document
clerkUserId ownership check.The browser reaches Convex with Clerk's own session token, which carries
aud: "convex" plus the email, name and given_name claims (Clerk's Convex
integration, configured under Sessions → Claims); the API route handlers mint
one from a JWT template named convex with the same claims. Both are signed by
the same issuer, which convex/auth.config.ts validates against
CLERK_JWT_ISSUER_DOMAIN with applicationID: "convex". requireUser returns identity.subject, denormalised
as clerkUserId on every table that has an owner (ownerClerkUserId and
readerClerkUserId on the list tables) — importantDates is the exception,
since it is scoped through the person it belongs to. SessionGuard is a UX
redirect for a session that vanished, and is not a security boundary.
Two independent systems. The first four buckets:
| Bucket | Limit | Where |
|---|---|---|
| AI generations | 10 per user per UTC day | recommendationUsage |
create_person | 50 per day | rateLimitBuckets |
create_date | 100 per day | rateLimitBuckets |
save_idea | 50 per day | rateLimitBuckets |
The list features add six more buckets to the same table (create_list_item,
import_list, invite_list, create_managed_list, list_photo_upload and
claim_list_item); the figures are in docs/security.md §4.
The generation counter is fail-closed and its reserve/refund mutations are
guarded by the shared CONVEX_SERVER_SECRET, so an authenticated browser client
cannot call refund and reset its own quota.
Seventeen Convex tables: people, importantDates, userSettings,
emailNotifications, recommendationUsage, rateLimitBuckets,
recommendations, savedIdeas, giftHistory, personShares, and the seven of
«Mi lista»: listItems, listAbout, listShares, listClaims, listBlocks,
listInviteEmails and managedLists. The map of what each one is for lives in
docs/estructura.md.
Two crons. "send daily birthday email reminders", 0 8 * * *, runs
internal.emails.runDailyEmailNotifications: it walks the users one at a time,
sends each of them batched HTML through the Resend REST API without the SDK, and
writes dedupe rows keyed by date, occurrence year and lead time, so a restart
cannot send the same reminder twice. "delete list photos that no item uses",
30 3 * * *, runs internal.lists.sweepOrphanPhotos and removes uploaded photos
that never ended up on a list item.
Next.js 16 (App Router) · React 19 · TypeScript 6 · Tailwind CSS 4 · Convex 1.46 · Clerk 7 · AI SDK 7 with Google Gemini 3.5 Flash · Zod 4 · react-hook-form · Base UI primitives following shadcn conventions · next-themes · Resend · Vitest.
Node 22 or newer, declared once in .nvmrc and mirrored by engines in
package.json. CI reads that same file rather than pinning a version of its own.
Do the accounts first, then let npx convex dev link the project before you set
anything on the Convex side — npx convex env set reads CONVEX_DEPLOYMENT out of
.env.local and exits with No CONVEX_DEPLOYMENT set without it. So the sequence
is: link, set the two Convex variables, run npx convex dev again.
"email": "{{user.primary_email_address}}", "name": "{{user.full_name}}" and "given_name": "{{user.first_name}}" (add email
to the template too). Convex reads them from the token (identity.email in
convex/settings.ts, the owner's name in convex/lists.ts): without email
the reminder cron has nobody to write to and stays silent.Then:
git clone https://github.com/jm-fuster/PickPal.git
cd PickPal
npm install
cp .env.example .env.local # fill it in — see Configuration below
Link the Convex project. This first push fails with "Environment variable CLERK_JWT_ISSUER_DOMAIN is used in auth config file but its value was not set" — that is expected, and the next two commands are what fix it:
npx convex dev --once # writes CONVEX_DEPLOYMENT and NEXT_PUBLIC_CONVEX_URL into .env.local
Generate the shared secret, and keep the value — it has to go in two places:
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
Put that 64-character string into CONVEX_SERVER_SECRET in .env.local, and then
give Convex the same one:
npx convex env set CLERK_JWT_ISSUER_DOMAIN https://your-issuer.clerk.accounts.dev
npx convex env set CONVEX_SERVER_SECRET paste-the-same-value-here
Now two terminals, both long-running:
npx convex dev # keeps watching and pushing convex/ while you work
npm run dev # http://localhost:3000
What works with only Clerk and Convex configured: the agenda, people, dates,
saved ideas and account deletion. POST /api/recommendations answers 503 until
the AI keys are in place, and emails silently never send. That is the designed
failure mode rather than a broken install.
The variables live in three different places, and conflating them is the usual way to lose an afternoon.
Next.js — .env.local locally, project settings on Vercel:
| Variable | Required | Notes |
|---|---|---|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | yes | Clerk → API keys (pk_test_…) |
CLERK_SECRET_KEY | yes | Clerk → API keys (sk_test_…) |
NEXT_PUBLIC_CLERK_SIGN_IN_URL | yes | /sign-in |
NEXT_PUBLIC_CLERK_SIGN_UP_URL | yes | /sign-up |
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL | yes | /agenda |
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL | yes | /agenda |
NEXT_PUBLIC_CONVEX_URL | yes | written by npx convex dev |
CONVEX_DEPLOYMENT | yes | written by npx convex dev |
CONVEX_SERVER_SECRET | yes | The same value must also exist in Convex. Missing here, /api/recommendations answers 503; present but different from the Convex one, it answers 429 No autorizado. |
GOOGLE_GENERATIVE_AI_API_KEY | yes | aistudio.google.com |
PEXELS_API_KEY | no | stock photos on idea cards; degrades quietly |
BRANDFETCH_CLIENT_ID | no | brand-store links; degrades quietly |
The Convex deployment — npx convex env set NAME value, plus --prod for
production:
| Variable | Required | Notes |
|---|---|---|
CLERK_JWT_ISSUER_DOMAIN | yes | the Issuer of the convex JWT template |
CONVEX_SERVER_SECRET | yes | the same value as in Next.js |
RESEND_API_KEY | for email | convex/emails.ts throws without it |
EMAIL_FROM | no | falls back to a PickPal <…> default |
Vercel only — CONVEX_DEPLOY_KEY. You generate it in the Convex dashboard,
not from the CLI.
CONVEX_DEPLOYMENT and NEXT_PUBLIC_CONVEX_URL,
which are local-only; the build derives the URL itself from CONVEX_DEPLOY_KEY.npx convex deploy --cmd 'npm run build', and
add CONVEX_DEPLOY_KEY.CLERK_JWT_ISSUER_DOMAIN at your production Clerk instance.npx convex env set CLERK_JWT_ISSUER_DOMAIN <prod issuer> --prod
and npx convex env set CONVEX_SERVER_SECRET <the same value you gave Vercel> --prod.
Until they exist, every build dies at npx convex deploy on the same auth-config
check as the local setup. If you have already pushed, set them and redeploy.CONVEX_DEPLOY_KEY decides which Convex deployment the build targets and
overwrites NEXT_PUBLIC_CONVEX_URL during the build, so changing only the URL
will not move you between deployments.
Every push to main redeploys. vercel.json skips Vercel builds for
dependabot/* branches, since CI already covers them.
The part I am most attached to, and the part you cannot see from the outside: the design system is checked against the code rather than merely described.
npm run token-map runs scripts/token-map.mjs, which joins a committed dump of
the Figma variables (design/figma-tokens.snapshot.json) with the real custom
properties in src/app/globals.css, writes the correspondence table to
docs/token-map.md, and exits non-zero only on
divergences, stale registry entries or conflicts that have not been declared in
design/token-divergences.json. A
check that goes permanently red teaches you to ignore it, so anything we chose to
live with has to be written down with a reason next to it. The current state is 71
variables mirrored and zero undeclared divergences.
Two other things that came out of doing it properly:
docs/design-system.md carries a
measured contrast ratio rather than an opinion.--primary was split into --primary and --brand, and --secondary into
--secondary and --brand-secondary, because a single token was being asked to
satisfy two contradictory WCAG requirements at once.On accessibility, the honest version: the skip link is real
(src/app/(app)/layout.tsx, href="#contenido"), the Figma file meets AA and the
code has caught up with it, aria-live appears in three places — five live regions
if you count the bare role="status" ones — rather than everywhere it could, and
lint still flags two raw <img> uses for the generated avatars.
npm test # 651 tests across 30 files
npm run lint
npm run build
CI (.github/workflows/ci.yml) runs on every push to main and every pull
request, on Node 22: npm ci, npx tsc --noEmit,
npx tsc -p convex/tsconfig.json --noEmit, then npm test. That second
type-check exists because Convex has its own tsconfig — without it, Convex-only
type errors sail through CI and fail the Vercel deploy instead. A second job
auto-squash-merges Dependabot patch and minor bumps once the first one is green.
CI does not yet run the linter or next build, so those two are on you before you
push.
| File | What it is |
|---|---|
docs/security.md | The trust model, eight mandatory patterns, a PR checklist keyed to whichever path you touched, an explicit "not implemented yet, and why" section, and a Dependabot post-mortem |
docs/ia-regalos.md | The AI pipeline and its three layers of defence. Stale on route paths and the store count — trust the code |
docs/email-notifications.md | The daily cron end to end, and why the toggle ships off |
docs/tech-stack.md | Why Convex over Prisma, Clerk over NextAuth, Resend without its SDK. Its intro still says "Next.js 14+" |
docs/figma-tokens.md | The normative rulebook for the Figma file: the four-layer model, alias rules, and the requirement that every token is born with a description |
docs/encargo-*.md | The brief for each feature, written before the code and closed with the decisions taken in its design interview: shared profiles, saved ideas by occasion, My list (one annex per phase) and lists kept for a child |
docs/token-map.md | Generated by npm run token-map |
docs/design-system.md | A decision log: 2,400 lines, no table of contents, and a dated work journal in its second half. Good for the reasoning, bad for looking up a token |
AGENTS.md and CLAUDE.md are this project's
contributor rulebook. They are short, and they exist to point at the three
documents you are expected to read before touching security, tokens or the design
system.
Some files under docs/ predate the current routes and survive only as history.
The routes in this README came out of npm run build.
Issues and pull requests are welcome. For anything large, open an issue first so
we can agree on the shape before you write it. See
CONTRIBUTING.md — and note that it is currently out of date in
a few places that this README supersedes. It opens with an English summary; the
rest of it, like everything under docs/, is in Spanish.
Found a security bug? Do not open an issue — SECURITY.md says
where to send it, in English and Spanish.
The code is © Jorge Molina Fuster, all rights reserved. Some of what ships alongside it is not mine:
/terminos.public/stores/ are their owners' trademarks, used for
identification./terminos.© Jorge Molina Fuster. All rights reserved. See LICENSE.