Otsukare is a two-sided freelance marketplace built around a simple bet: neither English nor Japanese is the primary language. Every job, proposal, message and review exists in both from the moment it is created, and a single agentic step handles the one part that genuinely needs judgement; everything else runs on deterministic triggers. It started life as a Lovable.dev scaffold and was hardened from there. It has no live users and no automated test suite yet.
The problem
Japan's freelance market and the pool of international talent willing to work in it are separated by language, not by skill or price. A platform that treats one language as primary and the other as a translation bolted on the side reproduces the friction it is meant to remove: someone is always reading a worse version of the conversation. Otsukare's answer is to make both languages first-class data rather than a display layer, so a Japanese client and an English-speaking freelancer each read a job, a message or a review natively, down to a translated voice message with its own synthesised audio.
What we built
29 Postgres tables under Supabase cover profiles, jobs, proposals, connections, messages, transactions, reviews and service packages, backed by 21 triggers and 31 SQL functions. 20 Supabase edge functions do the work: four handle admin operations, thirteen handle translation, and the rest cover the AI assistant, file validation and a one-off import of 856 bilingual skill tags across 21 categories, with 60 duplicates found and removed on import. 25 route pages and roughly 170 components, including about 52 shadcn/ui primitives, make up the frontend, built in React 18 and Vite. Escrow-held payments, real-time bilingual messaging and three-tier gig packages sit on top of that base.
How it runs
Content creation is the trigger. A job posted, a message sent, a review left, a proposal submitted, fires a database trigger that calls a translation edge function, and both language versions land in the same row before anyone reads either of them. A shared content-protection step masks emails, URLs, code and currency behind placeholders before any text reaches the translation API, then restores them afterwards, so nothing meant to stay literal gets mistranslated. Every translation call sits behind exponential-backoff retries, three attempts, at one, two and four seconds, with placeholder integrity checked before a result is accepted. The one agentic piece in the system is a Gemini-powered assistant scoped narrowly to helping a user fill in a form through function calling; it never touches the translation path itself.
Under the hood
122 row-level security policies across the 29 tables are what actually stop one user reading another's data, not application-level checks a route might forget to run. Security-definer functions set an explicit search path, closing a known class of schema-hijacking attack rather than leaving it to default behaviour. Deleting a user cascades through 31 foreign-key constraints from the auth table down to every related table, a full account removal in one transaction rather than a manual cleanup job. A trigger blocks marking a contract paid until a review actually exists: a small human-in-the-loop gate sitting inside otherwise deterministic plumbing.
What changed
The lesson worth publishing is not the feature list; it is where the deterministic and agentic line got drawn, and why. Everything with a correct, checkable outcome, translation, retries, cascading deletes, payment gating, runs as deterministic triggers and functions. The one place judgement is genuinely required, helping a person fill in an unfamiliar form, is the one place an agent runs, and it runs scoped to exactly that job. The build has no automated test suite yet, QA has run on manual, screenshot-driven checks, and it started life as a Lovable.dev scaffold before being hardened into this shape. Both facts are worth saying plainly rather than glossing over.
