[{"data":1,"prerenderedAt":154},["ShallowReactive",2],{"work-\u002Fwork\u002Fcreator-platform":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"tags":10,"role":17,"duration":18,"client":19,"client_named":6,"permission":6,"featured":20,"order":21,"body":22,"_type":147,"_id":148,"_source":149,"_file":150,"_stem":151,"_extension":152,"sitemap":153},"\u002Fwork\u002Fcreator-platform","work",false,"","Creator and fan subscription platform","Built the frontend for a subscription platform with three user roles — OAuth sign-in, Stripe subscriptions, real-time chat and role-based routing — against a backend being written in parallel.",[11,12,13,14,15,16],"Vue 3","TypeScript","Pinia","Ant Design Vue","Stripe","OAuth","Frontend engineer","4 months","A creator subscription startup",true,2,{"type":23,"children":24,"toc":140},"root",[25,34,40,45,51,56,61,67,78,88,98,108,118,124,129,135],{"type":26,"tag":27,"props":28,"children":30},"element","h2",{"id":29},"context",[31],{"type":32,"value":33},"text","Context",{"type":26,"tag":35,"props":36,"children":37},"p",{},[38],{"type":32,"value":39},"A startup building a platform where creators sell subscriptions and talk to their audience\ndirectly. Three roles — creator, fan, and admin — each with a different set of permissions,\nnavigation and data.",{"type":26,"tag":35,"props":41,"children":42},{},[43],{"type":32,"value":44},"Client and product details are withheld here; I do not have written permission to name them.",{"type":26,"tag":27,"props":46,"children":48},{"id":47},"the-hard-problem",[49],{"type":32,"value":50},"The hard problem",{"type":26,"tag":35,"props":52,"children":53},{},[54],{"type":32,"value":55},"The backend was being written at the same time as the frontend. Endpoints changed shape mid-build\nand some did not exist when the screens that needed them were due.",{"type":26,"tag":35,"props":57,"children":58},{},[59],{"type":32,"value":60},"On top of that, access control was not a logged-in-or-not question. A page could depend on sign-in\nstatus, email verification, subscription state and role at the same time, and those conditions\nchanged independently of each other.",{"type":26,"tag":27,"props":62,"children":64},{"id":63},"what-i-did",[65],{"type":32,"value":66},"What I did",{"type":26,"tag":35,"props":68,"children":69},{},[70,76],{"type":26,"tag":71,"props":72,"children":73},"strong",{},[74],{"type":32,"value":75},"Put the API behind one layer.",{"type":32,"value":77}," Token refresh, error normalisation and request handling lived in\na single place, so a change in an endpoint's shape was a change in one file rather than across\nevery screen calling it.",{"type":26,"tag":35,"props":79,"children":80},{},[81,86],{"type":26,"tag":71,"props":82,"children":83},{},[84],{"type":32,"value":85},"Made route guards composable.",{"type":32,"value":87}," Rather than one guard checking everything, each condition —\nauthenticated, verified, subscribed, correct role — was separate and combined per route. New\nroutes declared what they required instead of repeating the logic.",{"type":26,"tag":35,"props":89,"children":90},{},[91,96],{"type":26,"tag":71,"props":92,"children":93},{},[94],{"type":32,"value":95},"Built the Stripe subscription flow end to end:",{"type":32,"value":97}," plan selection, Stripe Elements checkout,\nwebhook-driven status changes, grace periods, cancellation and re-subscription. The awkward part is\nthat Stripe confirms asynchronously, so the interface has to stay honest about state it does not\nknow yet.",{"type":26,"tag":35,"props":99,"children":100},{},[101,106],{"type":26,"tag":71,"props":102,"children":103},{},[104],{"type":32,"value":105},"Added Google and Apple OAuth",{"type":32,"value":107}," behind one provider-agnostic interface, so a third provider would\nbe configuration rather than new code.",{"type":26,"tag":35,"props":109,"children":110},{},[111,116],{"type":26,"tag":71,"props":112,"children":113},{},[114],{"type":32,"value":115},"Built real-time chat",{"type":32,"value":117}," over WebSockets, with reconnection that recovers missed messages — the\nusers were on mobile connections that drop.",{"type":26,"tag":27,"props":119,"children":121},{"id":120},"outcome",[122],{"type":32,"value":123},"Outcome",{"type":26,"tag":35,"props":125,"children":126},{},[127],{"type":32,"value":128},"The platform shipped with all three roles working, and the frontend stayed buildable while the API\nunderneath it was still moving.",{"type":26,"tag":27,"props":130,"children":132},{"id":131},"stack",[133],{"type":32,"value":134},"Stack",{"type":26,"tag":35,"props":136,"children":137},{},[138],{"type":32,"value":139},"Vue 3, TypeScript, Pinia, Ant Design Vue, Stripe, Google and Apple OAuth, WebSockets.",{"title":7,"searchDepth":21,"depth":21,"links":141},[142,143,144,145,146],{"id":29,"depth":21,"text":33},{"id":47,"depth":21,"text":50},{"id":63,"depth":21,"text":66},{"id":120,"depth":21,"text":123},{"id":131,"depth":21,"text":134},"markdown","content:work:creator-platform.md","content","work\u002Fcreator-platform.md","work\u002Fcreator-platform","md",{"loc":4},1790598049477]