Yappa Knowledge Hub (YapHub)

Flagship

Slack-native AI knowledge capture, role-tailored digest pipeline, and bidirectional Notion synchronization

Symfony 7.3Symfony 7.3PHP 8.2PHP 8.2TypeScriptTypeScriptSlack BoltSlack BoltNotion APINotion APIOpenAIOpenAIDoctrine ORMDoctrine ORMSQLiteSQLitePlaywrightPlaywrightSentrySentry
Delivery Scope
13-Week Solo Internship (POC to MVP)
Backend Size
153 PHP & 45 TypeScript Files
Code Quality
PHPStan L8 β€’ 100% Core Entity Coverage
πŸŽ“ BACHELOR GRADUATION THESIS (SCRIPTIE)

Ontwerp en realisatie van een AI-ondersteund kennisdeelplatform voor Yappa (UCLL 2025-2026). Full 40+ page academic report covering problem formulation, 4 research questions, 50-flowchart exploration, and production MVP architecture.

Interactive Demo Player
4 CLIPS AVAILABLE
Slack Capture & Notion Sync with URL Extraction
// SYSTEM DEMO SPECIFICATION

Capturing insights from Slack shortcuts, validating external URLs against SSRF, generating Dutch AI highlights, and syncing to Notion.

Project context and stakeholders

Yappa is a digital agency in Bilzen (Belgium) where multidisciplinary teams (Hub Flow developers, online marketeers, and project managers) build custom web applications and digital platforms.

Valuable engineering solutions, framework updates, and client discoveries were regularly shared across Slack channels (#dev, #general) and internal knowledge sessions (YappaWaDoeDa). However, this knowledge was frequently buried in chat streams.

While Notion (β€œHet Orakel”) served as Yappa’s permanent documentation archive, manually documenting every insight created too much friction. The assignment was to bridge this gap: allow team members to capture knowledge directly in Slack, process and enrich it with AI summaries, and keep Notion automatically synchronized as the central source of truth.

Stakeholders and academic evaluation

  • Company promoter and code reviewer: Davy Dewit (Yappa)
  • Hub lead (Flow team): Wesley Lancel (Yappa)
  • Project manager: Laura Schrijvers (Yappa)
  • Academic supervisor: Mimi Willems (UC Leuven-Limburg / UCLL)
  • Degree program: Professional Bachelor in ICT, 2025-2026

Research questions and architectural evolution

The project answered four core engineering research questions:

  1. How can internal knowledge be captured without disrupting existing communication habits?
  2. How can collected knowledge be structured so it remains permanently findable and usable?
  3. How can raw knowledge items be transformed into role-tailored summaries for different team personas?
  4. How can the architecture remain extensible to future ingress/egress channels (email, RSS, podcasts)?

Exploration to production MVP

During the first two weeks, I mapped over 50 preliminary flowcharts evaluating Twig dashboards, Craft CMS plugins, monolithic C# engines, and autonomous agent loops (OpenClaw).

Validating a Proof of Concept (~150 Jira story points) proved that decoupling Slack as the ingress layer, Symfony 7.3 as the domain engine, and Notion as the consumption layer delivered the lowest user friction and highest maintainability.

Multi-layer system architecture

The platform operates across four decoupled layers:

Layer Component Technology Responsibility
1. Ingress Slack Bolt App TypeScript and Socket Mode Captures global shortcuts, message actions, and dynamic App Home views over persistent WebSockets
2. Ingress Security Content Extractor Symfony HTTPClient Validates external URLs against private IP ranges (SSRF defense), scrapes HTML, and truncates to 5,000 characters
3. Domain Engine Business Orchestrator Symfony 7.3 and PHP 8.2 DTO-driven workflow orchestrators, prompt templates, entity lifecycle, and async message dispatching
4. Persistence and Sync Dual-Store Resolver Doctrine ORM and Notion SDK SQLite local cache for immediate UI feedback, with bidirectional polling and instant Notion push

Key technical systems

1. Slack-native ingress and interactive App Home

Using Slack message shortcuts or modal triggers (views/modals/knowledge/saveMessage.ts), users capture text and links directly within their workflow.

The Slack application runs in Socket Mode, establishing an outbound WebSocket connection that eliminates the need to expose public HTTP webhook endpoints to the internet. The App Home tab renders dynamic category lists, subscriber counts, and manual digest generation controls.

View TypeScript implementation: Slack shortcut handler
// views/modals/knowledge/saveMessage.ts
export const handleSaveMessageShortcut = async ({ shortcut, ack, client }: SlackShortcutArgs) => {
  await ack();

  const messageText = shortcut.message.text ?? '';
  const messageTs = shortcut.message.ts;
  const channelId = shortcut.channel.id;
  const user = shortcut.user.id;

  await client.views.open({
    trigger_id: shortcut.trigger_id,
    view: buildKnowledgeModal({
      initialContent: messageText,
      sourceUrl: `https://yappa.slack.com/archives/${channelId}/p${messageTs.replace('.', '')}`,
      capturedBy: user
    })
  });
};

2. URL content extraction with SSRF defense

When shared messages contain web links, ContentExtractionService fetches the destination page with strict security controls:

  • Validates the target IP address against RFC 1918 private subnets and local loopback addresses to prevent Server-Side Request Forgery (SSRF).
  • Enforces a strict 10-second request timeout and verifies text/html content headers.
  • Isolates main readable article content and strips boilerplate scripts and navigation before passing text to the AI pipeline.
View PHP implementation: ContentExtractionService with SSRF defense
namespace App\Service\Content;

use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Component\HttpFoundation\IpUtils;

final readonly class ContentExtractionService
{
    private const array PRIVATE_RANGES = [
        '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '127.0.0.0/8', '::1/128'
    ];

    public function __construct(private HttpClientInterface $httpClient) {}

    public function extractContent(string $url): ?string
    {
        $host = parse_url($url, PHP_URL_HOST);
        if (!$host) return null;

        $ip = gethostbyname($host);
        if (IpUtils::checkIp($ip, self::PRIVATE_RANGES)) {
            throw new \InvalidArgumentException('SSRF blocked: Attempted request to private network address.');
        }

        $response = $this->httpClient->request('GET', $url, [
            'timeout' => 10.0,
            'max_redirects' => 3,
            'headers' => ['Accept' => 'text/html']
        ]);

        if ($response->getStatusCode() !== 200) return null;

        $html = $response->getContent();
        return $this->sanitizeAndExtractText($html);
    }
}

3. Role-based AI summarization and 7-step digest pipeline

The AI service (OpenAIService via symfony/ai-bundle) generates concise 2-3 sentence Dutch highlights using structured prompt templates (HIGHLIGHT_SUMMARY_PROMPT) with a low temperature setting for consistency.

Digests follow an automated 7-step pipeline:

  1. Trigger: Cron scheduler or manual Slack App Home button click.
  2. Collection: Queries unbundled knowledge items within the configured timeframe (weekly, biweekly, monthly).
  3. AI Compilation: Summarizes collected items into categorized highlights.
  4. Snapshot Storage: Persists immutable digest records to SQLite.
  5. Notion Publication: Builds and pushes rich Notion page blocks via NotionPageBuilder.
  6. Recipient Resolution: Maps active subscribers from SubscriptionRepository.
  7. Slack Distribution: Dispatches formatted direct message cards with individual DigestDelivery audit tracking.

4. Bidirectional Notion synchronization

The NotionRepositoryManager implements DatabaseResolverInterface to dynamically route requests whether the identifier is a local database integer ID or a Notion UUID. Knowledge items push to Notion immediately upon approval, while scheduled Symfony Messenger commands poll Notion every 2 minutes for updates made directly in the workspace.

View PHP implementation: NotionRepositoryManager dual-store sync
namespace App\Service\Notion;

use App\Repository\KnowledgeRepository;
use Symfony\Contracts\HttpClient\HttpClientInterface;

final readonly class NotionRepositoryManager implements DatabaseResolverInterface
{
    public function __construct(
        private KnowledgeRepository $knowledgeRepo,
        private HttpClientInterface $notionClient,
        private string $databaseId
    ) {}

    public function syncItemToNotion(int $knowledgeId): string
    {
        $item = $this->knowledgeRepo->find($knowledgeId) 
            ?? throw new \InvalidArgumentException("Item {$knowledgeId} not found");

        $payload = [
            'parent' => ['database_id' => $this->databaseId],
            'properties' => [
                'Title' => ['title' => [['text' => ['content' => $item->getTitle()]]]],
                'Category' => ['select' => ['name' => $item->getCategory()->getName()]],
                'Summary' => ['rich_text' => [['text' => ['content' => $item->getAiSummary() ?? '']]]]
            ]
        ];

        $response = $this->notionClient->request('POST', 'https://api.notion.com/v1/pages', [
            'json' => $payload
        ]);

        $responseData = $response->toArray();
        $notionUuid = $responseData['id'];
        
        $item->setNotionPageId($notionUuid);
        $this->knowledgeRepo->save($item, flush: true);

        return $notionUuid;
    }
}

Engineering quality, testing and CI/CD

Over the 13-week solo engagement (~60 working days), code quality was maintained through strict standards and code reviews:

  • Codebase scale: 153 PHP backend source files, 45 TypeScript Slack bot files.
  • Static analysis: Verified clean under PHPStan Level 8 with strict types enabled throughout.
  • Unit and integration testing: 100% PHPUnit 11 test coverage on core domain entities (Knowledge, Category, Subscription, Digest).
  • End-to-end testing: 10+ Playwright E2E test suites executing complete Slack-to-Notion user journeys with mocked external services (NOTION_MOCK=true, OPENAI_MOCK=true).
  • Code review rigor: 230+ code review comments addressed across 12 feature pull requests.
  • Technical debt reduction: Major mid-internship refactor overhauling 55 files (+3,543 / -11,135 LOC) to eliminate hardcoded dependencies.
  • Observability: Distributed Sentry tracing configured across both Symfony and Node.js runtimes with structured Monolog audit trails.