Specialudviklet kundeportal og backoffice
Mickeyr.dk Kundeportal
En specialudviklet Laravel-platform til SEO, opgaver, dokumenter og økonomi — med én enkel visning til kunden og et langt mere detaljeret backoffice bagved.
En kundeportal bliver hurtigt værdiløs, hvis den blot er endnu et sted, kunden skal logge ind for at finde de samme oplysninger, der i forvejen ligger i mails, regneark og forskellige tredjepartssystemer. Målet med portalen på portal.mickeyr.dk har derfor aldrig været at bygge et dashboard for dashboardets skyld. Den skal samle det, der faktisk er relevant i samarbejdet med mine kunder, og gøre det lettere at forstå og handle på.
En SEO-kunde skal kunne følge udviklingen uden at drukne i rå data fra forskellige værktøjer. En kunde med en udviklingsopgave skal kunne se, hvad der bliver arbejdet på, hvad der afventer dem, og hvad næste skridt er. Rapporter skal kunne findes igen, og fakturaer skal hænge sammen med den økonomi, jeg selv arbejder i. Samtidig skal kunden aldrig kunne se interne noter, tidsregistrering, tekniske kontroller eller faktureringsstatus — det hører udelukkende hjemme i mit eget backoffice.
Det lyder måske som én portal. Teknisk og funktionelt er det bevidst bygget som to.

Fiktive eksempeldata i skærmbillederne.
To brugere, to systemer
En af de første beslutninger i projektet var, at kundeportalen og mit interne administrationssystem ikke skulle være den samme brugerflade med forskellige menupunkter. De er adskilt hele vejen: kunden logger ind i kundeportalen, og jeg arbejder i et separat backoffice. De to sider kan sagtens dele de samme underliggende kunder, opgaver, dokumenter og fakturaer, men de har grundlæggende forskellige formål. Kunden skal have overblik. Backoffice skal kunne håndtere detaljerne.
Det betyder for eksempel, at en kunde kan se, at en opgave afventer materiale, mens jeg internt samtidig har tidsforbrug, tekniske noter, faktureringsstatus og næste interne handling liggende på den samme opgave. Det er de samme data — men det er ikke det samme behov. Princippet går igen i hele løsningen: kundens visning skal svare på de spørgsmål, kunden faktisk har, mens den interne visning gerne må være langt mere detaljeret.
Ikke-nørdet forklaring
Forestil dig et værksted. Kunden har brug for at vide, hvad der skal laves, hvornår bilen er klar, og hvad det kommer til at koste. Mekanikeren har samtidig brug for fejlkoder, reservedelsnumre, tidsregistrering og interne noter. Man kunne vise det hele på én skærm og bare skjule nogle felter for kunden — men jeg har i stedet valgt at bygge to forskellige arbejdsflader omkring de samme data. Det giver en renere oplevelse for kunden og en langt tydeligere sikkerhedsgrænse for mig.
Nørdet detalje
Løsningen er bygget i Laravel 13 med Filament v5 og MySQL, hvor kundeportal og backoffice er to separate paneler. Backoffice har bevidst ingen selvstændig offentlig loginside — en besøgende uden adgang bliver sendt tilbage til kundens login. Det er ikke i sig selv en sikkerhedsmekanisme; den egentlige adgangskontrol ligger bagved. Men der er heller ingen grund til at reklamere unødigt for, hvor administrationsområdet ligger.
Ingen kan bare oprette sig
Der findes ingen offentlig “Opret konto”-funktion — hverken til kundeportalen eller administrationen. Når en kunde skal have adgang, bliver brugeren oprettet administrativt og får derefter en mail, hvor vedkommende selv vælger sin adgangskode. Det giver mig kontrol over, hvem der tilknyttes hvilke virksomheder, samtidig med at jeg aldrig behøver kende eller vælge kundens kode.
Det er især vigtigt i en B2B-portal, hvor en bruger ikke bare er “en bruger”. Brugeren repræsenterer en konkret kunde og skal derfor kun kunne se oplysninger, der tilhører netop den kunde.
Sikkerheden ligger i datalaget — ikke i brugerfladen
En af de vigtigste arkitekturbeslutninger handler om noget, kunden forhåbentlig aldrig lægger mærke til: dataisolation. Bruger to virksomheder den samme portal, må virksomhed A naturligvis aldrig kunne se virksomhed B's oplysninger — heller ikke hvis nogen forsøger at ændre et ID i en URL, pille ved en formular eller kalde en funktion på en måde, den normale brugerflade aldrig ville gøre.
Derfor bliver kundetilhørsforholdet ikke først kontrolleret, når data skal vises. Det indgår allerede, når data bliver hentet. I praksis finder systemet ikke en opgave for derefter at spørge “må kunden se den?”. Det søger efter opgaven blandt netop denne kundes opgaver. Findes den ikke dér, findes den ikke for kunden.
Det er også derfor, at et forsøg på at tilgå en fremmed ressource giver “ikke fundet” frem for “adgang nægtet”. En besked om manglende adgang røber indirekte, at ressourcen findes. En 404 gør ikke. Kunden skal ikke kunne bruge systemets svar til at kortlægge data, vedkommende ikke har adgang til.
Adgangskontrollen stopper heller ikke ved login. Bliver en kunde eller bruger deaktiveret midt i en aktiv session, skal adgangen ikke fortsætte resten af dagen, bare fordi et login tidligere lykkedes — status bliver kontrolleret løbende, og adgangen stopper ved næste forespørgsel. Det er den type sikkerhed, der sjældent giver et flot skærmbillede til en case, men som afgør, om en portal bare ser professionel ud eller også opfører sig sådan.
Og fordi det er let at skrive “husk at filtrere på kunde” og lige så let at glemme det én gang, er isolationen mellem kunder dækket af automatiske tests. En ny funktion må ikke kunne åbne en vej til fremmede data, bare fordi en forespørgsel blev skrevet forkert. Vigtige garantier skal så vidt muligt håndhæves af systemet — ikke afhænge af, at jeg husker dem næste gang, jeg ændrer noget. Det princip dukker op igen i både fakturering, deployment og dokumentdeling.
Moduler frem for én stor portal
Ikke alle kunder har de samme behov. En hostingkunde har måske primært brug for fakturaer og dokumenter, en SEO-kunde skal kunne følge placeringer og backlinks, og en kunde med løbende udviklingsopgaver skal kunne følge opgaver, estimater og leverancer. Derfor er portalen bygget modulært, så funktioner kan slås til og fra pr. kunde. Løsningen kan udvides med nye områder, uden at portalen bliver mere kompleks for alle andre — en kunde skal ikke have seks tomme menupunkter, bare fordi en anden kunde bruger seks moduler.
SEO uden at sende kunden videre til et SEO-værktøj
SEO-modulet er et godt eksempel på, hvorfor jeg ønskede min egen portal. Der findes allerede mange glimrende SEO-platforme, men jeg ønsker ikke, at mine kunder skal lære et bestemt tredjepartssystem, forstå leverandørens metrics eller forholde sig til, hvor tallene kommer fra. De køber en SEO-ydelse af mig, og derfor bør oplevelsen også være min.

Fiktive eksempeldata i skærmbillederne.
Modulet kan trække på forskellige datakilder afhængigt af opgaven, men kundeoplevelsen er 100 % whitelabel. Kunden ser ikke navnet på den bagvedliggende dataleverandør — kunden ser sine egne søgeord, placeringer, udvikling, konkurrenter og links. Det interessante er resultatet, ikke hvilket API der leverede tallet.
SEO-data koster penge at hente, og forskellige typer data ændrer sig med forskellig hastighed. En søgeordsplacering kan være værd at tjekke flere gange i døgnet, en backlinkanalyse behøver ikke genkøbes hver sjette time, og konkurrentdata ændrer sig typisk endnu langsommere. Derfor arbejder systemet med forskellige friskhedsperioder — søgeord opdateres oftere end links, og links oftere end konkurrenter. Det interessante er, at planlæggeren ikke nødvendigvis henter data. Den spørger først, om data er gamle nok til, at det giver mening. Er de stadig friske, sker der ingenting. Det holder API-kaldene nede og gør omkostningerne mere forudsigelige.
Nørdet detalje
Hver datatype har sin egen friskhedslogik. En planlagt kommando kan køres som dry-run, så jeg kan se præcis, hvad systemet ville hente og hvorfor, uden at bruge penge på data. Saldoen hos dataleverandørerne bliver overvåget automatisk, og jeg får besked, før en lav saldo når at udvikle sig til manglende opdateringer. Ikke spektakulært — men den slags driftsoverblik er forskellen på en integration, der virker under en demo, og en, man kan stole på måned efter måned.
Jeg vælger de søgeord, der giver mening at overvåge, og kunden kan derefter følge dem i portalen. Ligger et søgeord uden for top 100, forsvinder det ikke fra rapporten — det bliver stående med en tydelig besked om, at det ikke aktuelt placerer sig i top 100. Det er bevidst. En rapport bliver ikke mere værdifuld af, at de dårlige resultater forsvinder. Flytter et vigtigt søgeord sig fra position 76 til 34, er det interessant. Flytter det sig fra 11 til 4, er det interessant. Og falder det fra 8 til 29, er det mindst lige så interessant. Portalen tracker derfor bevægelse begge veje — den er ikke bygget til kun at vise grønne pile.
Der findes mange flotte SEO-tal: trafikværdi, authority, spam score og en lang række andre sammensatte metrics. Nogle kan være nyttige i den rigtige sammenhæng. Men hvis jeg ikke mener, at et tal kan forklares ordentligt og bruges til noget konkret, skal det heller ikke stå i portalen bare for at få dashboardet til at se avanceret ud. Jeg viser hellere færre tal, der kan forsvares, end flere, der giver en illusion af præcision.
Eksterne API'er fejler, bliver langsomme og returnerer af og til mærkelige svar. Læste portalen direkte fra dataleverandøren, ville den slags hurtigt blive synligt som fejl, tomme grafer eller langsomme sider. Det gør den ikke. Data hentes i baggrunden og gemmes lokalt, og portalen viser altid de seneste gode målinger. Fejler et nyt opslag, har kunden stadig gårsdagens tal, mens systemet prøver igen i baggrunden. En kortvarig fejl hos en leverandør behøver med andre ord ikke blive kundens problem. Samtidig købes og behandles data i små enheder, så et enkelt søgeord, der fejler, ikke stopper de øvrige. Tre fejl i træk på det samme element udløser en besked til mig — men systemet spammer ikke med den samme alarm, og når en senere opdatering lykkes, bliver fejlhistorikken nulstillet. Kunden behøver ikke vide noget om det. Det er mit at håndtere.
Opgaver er mere end “åben” og “lukket”
Opgavemodulet begyndte med et enkelt spørgsmål: hvad skal kunden faktisk kunne se for at forstå, hvor en opgave står? Det er fristende at nøjes med én status — ny, i gang, afventer, færdig — men virkeligheden er mere nuanceret. En opgave kan godt være i gang og samtidig afvente materiale fra kunden. Den kan være færdigudviklet, men endnu ikke deployet. Leveret, men endnu ikke faktureret. Estimeret, men endnu ikke accepteret. Presses alt det ind i ét felt, bliver status hurtigt meningsløst.

Fiktive eksempeldata i skærmbillederne.
Derfor arbejder en opgave med flere uafhængige dimensioner: hvor i processen den befinder sig, hvem der aktuelt har ansvaret, om noget afventes og fra hvem, hvad der er estimeret, og hvordan den skal faktureres. Kundens visning samler det til noget, der er let at forstå. Backoffice må gerne beholde kompleksiteten.
Et estimat er en del af aftalegrundlaget. Bliver en opgave først estimeret til 8 timer og senere ændret til 14, skal det ikke se ud, som om der altid stod 14. Derfor overskrives estimater ikke — en ændring opretter en ny version, så både den oprindelige vurdering og de senere ændringer kan dokumenteres. Når et estimat sendes til kunden, låses det relevante aftalegrundlag, så tekster og forudsætninger ikke bagefter kan ændres til noget andet end det, kunden faktisk modtog. Det giver en mere fair proces for begge parter.
Opgavens historik er bygget som en log: nye hændelser bliver tilføjet, gamle bliver ikke redigeret væk, og der findes ikke en almindelig slettevej. Dermed kan et forløb altid følges bagefter — hvem gjorde hvad, hvornår skiftede status, hvornår blev estimatet sendt, hvornår blev opgaven deployet. Det er nyttigt ved store projekter, men måske endnu vigtigere ved de små opgaver, der ellers ender med at leve spredt ud over 37 mails hen over et halvt år.
Når en opgave er deployet, behøver den ikke stå som aktiv for evigt, bare fordi ingen får klikket “luk”. Systemet kan derfor selv afslutte den efter tre dage — men kun hvis kunden ikke har kommenteret i mellemtiden. Svarer kunden “det virker ikke helt på mobilen”, skal et automatisk job naturligvis ikke komme bagefter og erklære sagen afsluttet. Automatisering giver først værdi, når den også forstår, hvornår den ikke skal automatisere.
Økonomi uden magiske tal
Økonomimodulet er bygget på samme filosofi. Der skal være klare regler for, hvornår noget er en kladde, hvornår det er en rigtig faktura, og hvad der må ændres bagefter. Et fakturanummer bliver derfor først tildelt, når fakturaen faktisk udstedes. En kladde kan oprettes, ændres og slettes uden at bruge et nummer i rækken, men når fakturaen først er udstedt, behandles den som det dokument, den er. Den redigeres ikke bare bagefter — skal noget korrigeres, sker det gennem en kreditnota.

Fiktive eksempeldata i skærmbillederne.
Der ligger også bevidste regler om afrunding. Momsen beregnes på det summerede grundlag, og beløb afrundes kontrolleret én gang i stedet for at blive rundet af flere steder undervejs. Det er ikke en funktion, nogen kunde bliver begejstret over — men det er præcis den slags, et økonomisystem skal være kedeligt korrekt omkring.
Løbende aftaler kan automatisk oprette fakturakladder om natten, men systemet sender ikke regninger af sted på egen hånd. Et menneske skal stadig udstede dem, og det er bevidst. Der er mange ting, jeg gerne automatiserer — men at sende bindende økonomiske dokumenter uden mulighed for et sidste menneskeligt blik er ikke en af dem. Det samme gælder selve udsendelsen: at udstede en faktura og at sende den er to forskellige handlinger, så udstedelsen ikke ved en tilfældig bivirkning også kommer til at sende en mail. En opgave kan i øvrigt danne udgangspunkt for en fakturakladde og endda foreslå fakturering ud fra den registrerede tid — men nøgleordet er foreslå. Det er et værktøj til at spare manuelt arbejde, ikke en beslutningsmaskine.
Dokumenter kan deles uden at gøre storage offentlig
Et andet modul håndterer dokumenter og filer — rapporter, dokumentation eller andet materiale, der skal afleveres til en kunde. Kunden kan få adgang på to måder: gennem sin normale kundeportal eller gennem et særligt delingslink, der ikke kræver login. Det sidste er praktisk, når et dokument skal sendes til en person, der ikke har en bruger i portalen. De to adgangsveje er bevidst bygget uafhængigt af hinanden.

Fiktive eksempeldata i skærmbillederne.
Upload er ikke det samme som publicering. Uploader jeg fire filer og opdager, at den ene er forkert, skal kunden ikke nødvendigvis nå at se den. Derfor er upload og publicering to forskellige handlinger: filer kan forberedes, kontrolleres og skiftes ud, og først derefter bliver materialet gjort tilgængeligt. En lille forskel i brugerfladen — en stor forskel i arbejdsgangen.
Et dokument kan bestå af flere filer, og delingslinket peger derfor på dokumentets oversigt frem for direkte på den første fil. Dermed kan jeg senere tilføje eller udskifte filer uden at sende kunden et helt nyt link, og kunden får samtidig et klart overblik over det delte materiale. Selv om et dokument kan åbnes gennem et offentligt link, ligger filerne ikke i en offentlig mappe. Det offentlige link giver adgang gennem applikationen, ikke direkte til storage, og når en fil skal vises eller hentes, kontrollerer systemet først, at den rent faktisk hører til det dokument, som det pågældende delingstoken giver adgang til. Man kan derfor ikke tage linket til én fil, ændre filens ID og begynde at hente andre kunders dokumenter.
Nørdet detalje
Et delingstoken tilhører præcis ét dokument, og filerne findes gennem dokumentrelationen — ikke omvendt. Det er det samme designprincip som resten af portalen: adgang skal begrænses dér, hvor data bliver hentet, ikke kun i det link eller den knap, brugeren ser. Samme dokument kan i øvrigt have flere delingslinks, hver med sin egen udløbsdato, sin egen historik og sin egen tilbagekaldelse — så ét link kan lukkes uden at røre de andre.
Delingsfunktionen registrerer, hvornår et link blev åbnet, hvornår oversigten blev vist, og hvornår en fil blev åbnet eller downloadet. Her dukkede en klassisk detalje fra den virkelige verden op: mange mailsystemer og sikkerhedsprodukter scanner links automatisk, før mailen når frem. Talte sådan et besøg som en rigtig åbning, ville nogle dokumenter se ud til at være åbnet nærmest samtidig med, at mailen blev sendt. Derfor bliver automatiske scannersignaler gemt, men tæller ikke med som en reel menneskelig åbning. Det giver mindre imponerende statistik — men mere sand statistik.
Sikkerhed er også det, der sker uden for Laravel
Applikationen er kun ét lag. På serveren kører portalen isoleret med sin egen systembruger, sin egen PHP-procespulje og sin egen database, og filadgangen er begrænset. De offentlige delingssider har en stram Content Security Policy, og hele portalhosten er afvist i robots.txt, fordi en kundeportal ikke har noget at gøre i Googles indeks. Delingstokens bliver desuden redigeret ud af maillogs, så et følsomt link ikke ender som almindelig tekst i en logfil.
Uploadede filer behandles også defensivt. Systemet stoler ikke blindt på det filnavn eller den MIME-type, klienten sender med — filtypen vurderes ud fra filens faktiske indhold. Formater som SVG og HTML bliver aldrig vist direkte inline fra dokumentområdet, så en fil, der ser uskyldig ud ved upload, ikke bagefter kan bruges som aktivt indhold i browseren.
Det bedste testværktøj viste sig at være produktion
Der er automatiske tests. Mange af dem — i skrivende stund 289. En stor del handler om adgangskontrol, kundeadskillelse og de regler, der ikke må brydes, når systemet bliver videreudviklet. Men projektet har også bekræftet noget andet: en grøn testsuite betyder ikke, at arbejdet er færdigt. Tre af de mest lærerige fejl blev ikke fundet ved at læse koden, men ved at kontrollere den faktisk deployede løsning.
Et release manglede dele af maildesignet. En regel i den proces, der byggede releasepakken, kom utilsigtet til at ekskludere mailtemaet. Koden så korrekt ud, og testene kunne være grønne — men filerne kom simpelthen ikke med på serveren. Løsningen blev ikke “husk at kigge efter det næste gang”, men en automatisk kontrol, der får deploymentet til at fejle, hvis de nødvendige filer mangler.
Den rigtige kode lå i det forkerte arkiv. En anden fejl skyldtes et gammelt releasearkiv: den korrekte kode fandtes, men det var ikke den, der blev deployet. Også her kunne man have nøjedes med en note om at huske at tjekke datoen — i stedet blev processen ændret, så systemet selv opdager uoverensstemmelsen.
Fakturaen virkede — mailen gjorde ikke. En faktura kunne oprettes korrekt, men en konfigurationsforskel på serveren betød, at selve fakturamailen kunne fejle, når den blev behandlet gennem køsystemet. Det var ikke nok at kontrollere, at fakturaen fandtes; hele flowet skulle testes hele vejen — fra handling til kø til mail til det faktiske resultat.
Nørdet detalje
Ingen af de tre fejl blev lukket med “nu ved jeg det”. De blev lukket med kontroller. Det er måske det vigtigste tekniske princip i hele projektet: er noget vigtigt nok til, at det aldrig må ske igen, bør løsningen så vidt muligt selv opdage det. Mennesker er gode til at løse nye problemer. Computere er gode til at huske de gamle.
Ikke-nørdet forklaring: hvad er der egentlig bygget?
Fjerner man Laravel, middleware, køer, tokens og API-kald fra historien, er det ret enkelt: jeg har bygget ét samlet sted til mine kunders samarbejde med mig. Her kan de følge SEO-resultater, opgaver, dokumenter og økonomi, og de får kun vist det, der er relevant og forståeligt for dem. Bagved har jeg et separat arbejdsområde til alle de detaljer, der skal til for at levere ydelsen. Når eksterne tjenester fejler, forsøger systemet at håndtere det uden at gøre det til kundens problem. Når noget økonomisk skal sendes, kræver det stadig et menneskeligt valg. Når et dokument deles, er filen stadig beskyttet. Og opdager jeg en fejl i driften, forsøger jeg ikke bare at rette den — jeg forsøger at bygge en kontrol, der fanger den næste gang.
Bygget til det næste modul — ikke kun det nuværende
Portalen er ikke tænkt som et færdigt produkt med en fast liste af funktioner. Den er fundamentet for den måde, jeg gerne vil arbejde med kunder på fremover, og nye moduler kan komme til, uden at alle eksisterende kunder automatisk får mere kompleksitet. Det kan være nye former for rapportering, godkendelsesflows, integrationer, hostingstatus, automatiserede analyser eller serviceaftaler — eller noget, jeg endnu ikke har haft brug for. Det afgørende er, at fundamentet allerede forstår kunder, brugere, adgang, moduler, historik og grænserne mellem intern og ekstern information. Det gør den næste funktion lettere at bygge rigtigt.
Resultatet
Kundeportalen er gået fra idéen om “et sted, hvor kunden kan se sine ting” til en samlet platform for en stadig større del af samarbejdet mellem mig og mine kunder. Løsningen rummer i dag blandt andet:
- Kundeportal og separat internt backoffice
- Modulbaseret adgang pr. kunde
- Stram isolation mellem kundernes data — dækket af tests
- SEO-rapportering med whitelabel-data
- Automatisk og omkostningsbevidst opdatering af SEO-data
- Fejltolerant håndtering af eksterne datakilder
- Opgavestyring med versionerede estimater og fuld historik
- Fakturaer, kreditnotaer og automatiske kladder med menneskelig godkendelse
- Privat dokumenthåndtering med delingslinks, udløb og tilbagekaldelse
- Detaljeret hændelsessporing og driftsovervågning
- Servermæssig isolation og 289 automatiske tests
Men antallet af funktioner er ikke det vigtigste resultat. Det vigtigste er, at kompleksiteten ligger det rigtige sted. Kunden behøver ikke forstå min dataleverandør, fakturastatusserne bagved eller hvordan en dokumentfil bliver autoriseret. Kunden skal kunne få svar på: Hvordan går det? Hvad bliver der arbejdet på? Er der noget, jeg skal gøre? Hvad har vi aftalt? Hvor finder jeg materialet? Det er de spørgsmål, portalen skal besvare. Resten er mit arbejde.
Specialudvikling giver mening, når arbejdsgangen er vigtigere end standardsystemet
Jeg kunne have samlet en håndfuld eksisterende SaaS-værktøjer og givet kunderne adgang til hvert af dem — ét til SEO, et til opgaver, et til dokumentdeling, et til fakturaer og et femte til rapportering. Det havde været hurtigere i begyndelsen, men det havde ikke skabt den kundeoplevelse eller den interne sammenhæng, jeg ønsker. I stedet er løsningen bygget omkring selve arbejdsgangen. Det er også den type specialudvikling, jeg finder mest interessant: ikke software for softwarens skyld, men systemer, der fjerner unødvendige mellemled, samler informationen og gør en konkret proces lettere at arbejde i.
Skal jeg bygge noget tilsvarende til din virksomhed?
Mange virksomheder ender med en kombination af regneark, mails, Dropbox-mapper, CRM-systemer, projektværktøjer og manuelle rutiner, som hver især fungerer fint — men ikke fungerer særlig godt sammen. Det er ofte dér, en specialudviklet portal eller et internt system begynder at give mening. Jeg hjælper med at afklare arbejdsgangen først og teknologien bagefter: løser et eksisterende standardsystem opgaven bedst, siger jeg det. Ligger behovet derimod mellem systemerne, kan jeg bygge præcis den del, der mangler.
Har I en arbejdsgang, der er vokset fra standardværktøjerne?
Jeg bygger kundeportaler, interne systemer og integrationer omkring den måde, virksomheden faktisk arbejder på — med fokus på ejerskab, sikkerhed og mulighed for at bygge videre.
Skal vi tage en snak?
Fortæl kort om din opgave — så vender jeg tilbage med et ærligt bud på, hvad der giver mening.